이번에는 게시글 및 댓글의 CRUD가 가능하도록 자유게시판을 만들어보려고 합니다.
1. FreeBoardEntity.java
entit패키지에 새로운 자바 클래스를 생성해줍니다.
@Entity
@Data
@Table(name="freeboard")
public class FreeBoardEntity
{
@Id
@GeneratedValue( strategy = GenerationType.IDENTITY )
private Integer frboardid;
private String frboardwriter;
private String frboardtitle;
private String frboardcontent;
private Integer frboardhits; //조회수
@Column(name = "frboardcreatetime")
private LocalDateTime frboardcreatetime; //생성일자
@Column(name = "frboardupdatetime")
private LocalDateTime frboardupdatetime; //수정일자
private String tags;
}
2. FreeboardRepository.java 작성
FreeboardEntity에 대한 CRUD작업을 제공하는 JpaRepository를 확장하는 인터페이스인 FreeboardRepository.java를 작성합니다.
@Repository
public interface FreeBoardRepository extends JpaRepository<FreeBoardEntity, Integer>{
}
3. 글 쓰기 기능 구현
먼저 글작성 기능을 구현하는 과정에 대해 작성하려고 합니다.
3-1.FreeBoardService.java 작성
@Service
public class FreeBoardService {
@Autowired
private FreeBoardRepository frbreBoardRepository;
// 글 작성하기
public void FreeBoardWrite( FreeBoardEntity entity ) {
frbreBoardRepository.save(entity);
}
}
3-2. FreeboardController.java 작성
@Controller
@RequestMapping("/community/freeboard")
public class FreeboardController {
// 게시글 작성 페이지을 표시
@GetMapping("/write")
public String commFreeBoardWriteForm(Model model, Principal principal) {
// 현재 로그인한 사용자의 이름을 가져옴
String username = principal.getName();
model.addAttribute("user", username);
return "community/commfreeboardwrite";
}
// 작성된 게시글을 처리하고 데이터베이스에 저장
@PostMapping("/writedo")
public String commFreeBoardWritedo( FreeBoardEntity frbEntity, Model model) {
LocalDateTime LDTime = LocalDateTime.now(); // LocalDateTime을 호출하여 현재시간을 가져옴
frbEntity.setFrboardcreatetime(LDTime); // 게시글 엔티티의 생성시간을 현재시간으로 설정함
frbEntity.setFrboardhits(0); // 조회수를 0으로 설정함
frbService.FreeBoardWrite( frbEntity ); //서비스의 해당 메서드에 게시글 엔티티를 전달하여 데이터베이스에 저장
return "redirect:list";
}
3-3. 프론트엔드 html
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://thymeleaf.org">
<head>
//생략
</head>
<body>
<div class="container">
<div class="row">
<div class="col-lg-9">
<div class="layout">
<form action="/community/freeboard/writedo" method="post">
<h3>게시글 작성</h3> <br/>
<div class="sub">
제목 <input type="text" name="frboardtitle" style="font-size: 16px;">
작성자 <input type="text" th:value="${user}" name="frboardwriter" style="font-size: 16px;" readonly>
태그 <input type="text" name="tags" style="font-size: 16px;">
</div>
<div class="sub">
</div>
<textarea name="frboardcontent" id="frboardcontent"></textarea>
<input type="submit" value ="작성">
</form>
</div>
</div>
</div>
</div>
</body>
</html>
CKEDITOR
웹기반의 텍스트 에디터로 HTML을 생성하고 수정할수 있도록 도와주는 도구입니다.
텍스트작성부분은 ckeditor을 사용하여 구성하였습니다.
https://www.codingfactory.net/13253
4. 글목록 부분 구현
4-1.FreeBoardService.java 추가
@Service
public class FreeBoardService {
@Autowired
private FreeBoardRepository frbreBoardRepository;
//.. 글 작성부분
// 글 리스트 불러오기
public Page< FreeBoardEntity > freeBoardList( Pageable pageable ){
return frbreBoardRepository.findAll( pageable );
}
}
Page<FreeBoardEntity>
Page<>는 Spring Data JPA의 페이징처리를 위한 인터페이스입니다.
Pageable은 Spring Data의 일부로, 페이지네이션을 위한 정보(페이지 번호, 페이지당 항목 수, 정렬 등)를 담고 있습니다.
findAll
연결된 데이터베이스 테이블의 모든 행을 조회하고, 각 행을 해당 엔티티 클래스의 인스턴스로 변환하여 반환합니다.
여기에서는 Pageable 객체를 매개변수로 받아, 해당 조건에 맞게 페이지처리된 게시글데이터를 Page<FreeBoardEntity> 형태로 반환해줍니다.
4-2. FreeboardController.java 추가
@Controller
@RequestMapping("/community/freeboard")
public class FreeboardController {
@Autowired
private FreeBoardService frbService;
@GetMapping("/write")
// 게시글 작성부분
// 게시판 목록 페이지 표시, Pagination을 사용하여 게시글을 페이지별로 나눔
@GetMapping("/list")
public String commFreeBoardList( Model model , @PageableDefault( page = 0, size = 10, sort = "frboardid", //첫페이지를0으로 설정하고 페이지당 10개의 게시글을 출력, frboardid필드를 기준으로 내림차순
direction=Sort.Direction.DESC ) Pageable pageable) {
Page<FreeBoardEntity> list = frbService.freeBoardList(pageable);
int nowpage = list.getPageable().getPageNumber() + 1; //현재페이지번호
int startpage = Math.max( nowpage - 3 , 1); //시작페이지번호
int endpage = Math.min( nowpage+3 , list.getTotalPages()); //종료페이지번호
model.addAttribute("nowpage", nowpage);
model.addAttribute("startpage", startpage);
model.addAttribute("endpage", endpage);
model.addAttribute("list", frbService.freeBoardList( pageable ));
return "community/commfreeboardlist";
}
}
Page
1. 현재 페이지 번호 (nowpage): 사용자가 현재 보고 있는 페이지 번호입니다. 이는 페이지네이션 컨트롤에서 '현재 페이지'를 강조하는 데 사용됩니다.
getPageNumber() 메소드는 현재 페이지의 인덱스를 반환합니다. 일반적으로 페이지 인덱스는 0부터 시작하므로, 사용자 친화적인 표시를 위해 1을 더해줍니다.
시작 페이지 번호 (startpage) 및 종료 페이지 번호 (endpage): 이들은 페이지네이션 바에서 표시할 페이지 범위를 결정합니다. 예를 들어, 사용자가 5번 페이지를 보고 있다면, 시작 페이지는 2, 종료 페이지는 8로 설정될 수 있습니다.
이렇게 하면 사용자는 현재 페이지 주변의 페이지로 쉽게 이동할 수 있습니다.
startpage는 현재 페이지에서 3을 뺀 값이나, 최소 1로 설정합니다. endpage는 현재 페이지에서 3을 더한 값이나, 최대 페이지 수로 설정합니다.
freeBoardList(pageable) 메서드는 Pageable 객체에 따라 필요한 페이지의 게시글 목록을 반환합니다.
4-3. 프론트엔드 html
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://thymeleaf.org">
<head>
<!-- 생략 -->
</head>
<body>
<div class="loggedin-status" th:if="${#authorization.expression('isAuthenticated()')}" style="display: block;"></div>
<div class="container">
<div class="row">
<!-- Main content -->
<div class="col-lg-9 mb-3">
<div class="row text-left mb-5"> </div>
<!-- End of post 1 -->
<h4>자유게시판</h4><br/>
<div th:each="freeboard : ${list}" class="card row-hover pos-relative py-3 px-3 mb-3 border-warning border-top-0 border-right-0 border-bottom-0 rounded-0">
<div class="row align-items-center">
<div class="col-md-8 mb-3 mb-sm-0">
<h5>
<a th:text="${freeboard.frboardtitle}" style="color: black;" th:href="@{/community/freeboard/view(id=${freeboard.frboardid})}"></a>
</h5>
<p class="text-sm">
<span th:text="${#temporals.format(freeboard.frboardcreatetime, 'yyyy-MM-dd')}" class="text-black"></span>
<span class="op-6"> by </span>
<a class="text-black" th:text="${freeboard.frboardwriter}"></a>
</p>
<div th:if="${freeboard.tags != null}" class="text-sm op-5">
<span th:each="tag : ${freeboard.tags.split(',')}">
<a class="text-black mr-2" th:href="'#'" th:text="'#' +${tag.trim()}"></a>
</span>
</div>
</div>
<div class="col-md-4 op-7">
<div class="row text-center op-7">
<div class="col px-1">
<i class="ion-ios-chatboxes-outline icon-1x"></i>
<span class="d-block text-sm" th:text="${commentCounts[__${freeboard.frboardid}__]} + ' 댓글' "></span>
</div>
<div class="col px-1">
<i class="ion-ios-eye-outline icon-1x"></i>
<span class="d-block text-sm" th:text="${freeboard.frboardhits} + ' 조회' "></span>
</div>
</div>
</div>
</div>
</div>
<div class="paging_block">
<ul class="pagination">
<!-- Previous Page Link -->
<li class="page-item" th:if="${list.hasPrevious()}">
<a class="page-link" th:href="@{/community/freeboard/list(page=${list.number - 1})}" aria-label="Previous">
<span aria-hidden="true">«</span>
</a>
</li>
<!-- Page Number Links -->
<li class="page-item" th:each="pageNum : ${#numbers.sequence(startpage, endpage)}" th:class="${pageNum - 1 == list.number ? 'active' : ''}">
<a class="page-link" th:href="@{/community/freeboard/list(page=${pageNum - 1})}" th:text="${pageNum}"></a>
</li>
<!-- Next Page Link -->
<li class="page-item" th:if="${list.hasNext()}">
<a class="page-link" th:href="@{/community/freeboard/list(page=${list.number + 1})}">
<span aria-hidden="true">»</span>
</a>
</li>
</ul>
</div>
<a class="btn btn-lg btn-block btn-success rounded-0 py-4 mb-3 bg-op-6 roboto-bold write-button"
style="background-color: #7A297B;" th:href="@{/community/freeboard/write}">게시글 작성</a>
</div>
</div>
</div>
</body>
</html>
5. 글상세보기 부분구현
목록페이지에서 게시글제목을 클릭했을때 출력되는 글상세보기 페이지를 구현할 것입니다.
5-1. FreeBoardService.java 구문추가
@Service
public class FreeBoardService {
@Autowired
private FreeBoardRepository frbreBoardRepository;
// 글 작성하기
// 글 리스트 불러오기
// 글 상세보기 페이지 불러오기
public FreeBoardEntity freeBoardView( Integer id ) {
return frbreBoardRepository.findById( id ).get() ;
}
findById(id)
데이터베이스에서 주어진 id를 가진 FreeBoardEntity 객체를 찾습니다. 만약 해당 ID의 게시글이 데이터베이스에 존재한다면, 이 메서드는 Optional<FreeBoardEntity> 타입의 객체를 반환합니다.
5-2. FreeboardController.java 추가
@GetMapping( "/view" )
public String boardView( Model model , Integer id, FreeBoardEntity frbEntity) {
FreeBoardEntity frbTemp = frbService.freeBoardView(id); //id에 해당하는 게시글을 불러옵니다.
if(frbTemp.getFrboardhits() == null) {
int hits = 0 ; //조회수가null일경우0으로 설정하고,
frbTemp.setFrboardhits( hits );
}else {
int hits = frbTemp.getFrboardhits() + 1 ; //그렇지않으면 조회수를 1증가시킵니다.
frbTemp.setFrboardhits( hits );
}
frbService.FreeBoardWrite( frbTemp );
model.addAttribute( "freeboard", frbService.freeBoardView(id)); //상세보기할 게시글정보를 model에 추가합니다.
return "community/commfreeboardview"; //해당페이지로 return합니다.
}
5-3. 프론트엔드 html
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://thymeleaf.org">
<head>
<!-- 생략 -->
</head>
<body>
<!-- Navbar Start -->
<nav class="navbar navbar-expand-lg bg-white navbar-light shadow sticky-top p-0">
<a href="/" class="navbar-brand d-flex align-items-center px-4 px-lg-5">
<h2 class="m-0 text-primary" style="font-family: 'Nanum Gothic', sans-serif;">
<img src="/img/winelogo.jpg" alt="Image" width="50" height="50">
KOWASA <h5>한국와인 정보 커뮤니티</h5>
</h2>
</a>
<button type="button" class="navbar-toggler me-4" data-bs-toggle="collapse" data-bs-target="#navbarCollapse">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarCollapse">
<div class="navbar-nav ms-auto p-4 p-lg-0">
<a href="/" class="nav-item nav-link active">Home</a>
<div class="nav-item dropdown">
<a href="/search" class="nav-item nav-link active">와인검색</a>
</div>
<div class="nav-item dropdown">
<a href="#" class="nav-link dropdown-toggle" data-bs-toggle="dropdown">커뮤니티</a>
<div class="dropdown-menu fade-up m-0">
<a href="/community/freeboard/list" class="dropdown-item">자유게시판</a>
<a href="/community/eventboard/list" class="dropdown-item">행사정보</a>
</div>
</div>
<div class="nav-item dropdown">
<a href="#" class="nav-link dropdown-toggle" data-bs-toggle="dropdown">와인파트너스</a>
<div class="dropdown-menu fade-up m-0">
<a href="/partners/producer/list" class="dropdown-item">생산자</a>
<a href="/partners/shop/list"class="dropdown-item">샵/레스토랑 정보</a>
<a href="/partners/job/list" class="dropdown-item">구인구직</a>
</div>
</div>
</div>
<a href="/login" class="btn btn-primary py-4 px-lg-5 d-none d-lg-block" th:if="${#authorization.expression('isAnonymous()')}">
Login<i class="fa fa-arrow-right ms-3"></i>
</a>
<div class="btn btn-primary py-4 px-lg-5 d-none d-lg-block" th:if="${#authorization.expression('isAuthenticated()')}">
<p style="margin-top: 10px;"><span th:text="${#authentication.name}"></span>님, 환영합니다.</p>
<button style="padding: 2px 5px;" onclick="location.href='/logout'">로그아웃</button>
<button style="padding: 2px 5px;" onclick="location.href='/mypage'">마이페이지</button>
</div>
</div>
</nav>
<!-- Navbar End -->
<!-- Back to Top -->
<a href="#" class="btn btn-lg btn-primary btn-lg-square back-to-top"><i class="bi bi-arrow-up"></i></a>
<!-- Page content-->
<div class="container mt-5">
<div class="row">
<div class="col-lg-8">
<!-- Post content-->
<article>
<!-- Post header-->
<header class="mb-4">
<!-- Post title-->
<h1 class="fw-bolder mb-1"> <a th:text="${freeboard.frboardtitle}"></a> </h1>
<!-- 작성자 및 등록일 -->
<div class="text-muted fst-italic mb-2">
작성자 <a th:text="${freeboard.frboardwriter}"></a>
| 작성일 <a th:text="${freeboard.frboardcreatetime.toLocalDate()}"></a>
| 조회수 <a th:text="${freeboard.frboardhits}"></a>
</div>
</header>
<!-- 글 내용 -->
<section class="mb-5">
<div class="content-section" th:utext="${freeboard.frboardcontent}"></div>
</section>
</article>
</div>
</div>
</div>
</body>
</html>
6. 글 삭제하기
게시판을 삭제하는 부분을 구현해보겠습니다.
6-1. FreeBoardService.java 구문추가
@Service
public class FreeBoardService {
@Autowired
private FreeBoardRepository frbreBoardRepository;
// 글 작성하기
// 글 리스트 불러오기
// 글 상세보기 불러오기
// 글 삭제하기
public void freeBoardDelete( Integer id ) {
// 게시글 삭제
frbreBoardRepository.deleteById(id);
}
@Autowired
FreeBoardRepository라는 필드에 대한 의존성 주입을 나타냅니다.
freeBoardDelete메소드 특정id를 가진 게시물을 데이터베이스에서 삭제합니다.
6-2. FreeBoardController.java 구문추가
@Controller
@RequestMapping("/community/freeboard")
public class FreeboardController {
@Autowired
private FreeBoardService frbService;
// 게시판 목록
// 게시글 작성
// 게시판 상세보기
// 게시글 삭제
@PostMapping( "/delete" )
public String boardDelete( @RequestParam("id") Integer id) {
frbService.freeBoardDelete(id);
return "redirect:list";
}
@RequestParam("id") Integer id
id라는 이름의 파라미터를 정수형으로 받아들이도록 지시, 이파라미터는 삭제할 게시물의 id를 나타냅니다.
restful형식을 준수하자면 http메서드 @deleteMapping으로 작성해야된다고 알고 있었으나 구글링을 해본결과 편의성을 따졌을때 POST 메서드 요청 메세지를 보내는 것도 나쁘지 않은 방법이라고 합니다.
참고- [API 설계] DELETE request 요청/처리/응답에 관한 소소한 고민
https://humblego.tistory.com/18
6-3. 프론트엔드 부분 작성하기 Freeboard.js
// AJAX를 사용하여 댓글 삭제 처리
function deleteComment(commentId) {
if (confirm('댓글을 삭제하시겠습니까?')) {
$.ajax({
url: `/community/freeboard/comment/` + commentId + `/delete`,
type: 'POST',
success: function(result) {
$('#comment-' + commentId).remove();
},
error: function(error) {
alert("댓글 삭제에 실패했습니다.");
}
});
}
}
$(document).ready(function() {
$('#delete-post-form').on('submit', function(event) {
var confirmed = confirm('게시글을 삭제하시겠습니까?');
if (!confirmed) {
event.preventDefault();
}
});
});
7. 글 수정하기
마지막으로 작성한 글을 수정하는 부분을 작성해보겠습니다.
7-1. FreeBoardController.java 구문추가
// 게시글 수정
@GetMapping( "/modify/{id}" )
public String boardModify( @PathVariable( "id" ) Integer id, Model model) {
model.addAttribute("freeboard", frbService.freeBoardView(id));
return "community/commfreeboardmodify";
}
// 수정된 게시글을 처리하고 데이터베이스에 업데이트
@PostMapping( "/update/{id}" )
public String boardUpdate( @PathVariable( "id" ) Integer id, @ModelAttribute FreeBoardEntity frbEntity) {
FreeBoardEntity boardTemp = frbService.freeBoardView(id);
boardTemp.setFrboardtitle(frbEntity.getFrboardtitle());
boardTemp.setFrboardcontent(frbEntity.getFrboardcontent());
LocalDateTime LDTime = LocalDateTime.now();
boardTemp.setFrboardupdatetime(LDTime);//수정시간을 현재시간으로 설정합니다
boardTemp.setTags(frbEntity.getTags());
frbService.FreeBoardWrite(boardTemp);
return "redirect:/community/freeboard/view?id=" + id;
//수정 작업 후에는 사용자를 수정된 게시글의 상세 보기 페이지로 리다이렉트합니다.
//이때 해당 게시글의 ID를 쿼리 파라미터로 전달합니다.
}
7-2.프론트엔드 추가 html
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://thymeleaf.org">
<head>
//생략
<script src="https://cdn.ckeditor.com/4.16.0/standard/ckeditor.js"></script>
</head>
<body>
<div class="layout">
<form th:action="@{/community/freeboard/update/{id}(id=${freeboard.frboardid})}" method="post">
<div class="sub">
제목: <input type="text" name="frboardtitle" th:value="${freeboard.frboardtitle}"> 작성자 :<input type="text" th:value="${freeboard.frboardwriter}" name="frboardwriter">
</div>
<textarea name="frboardcontent" id="frboardcontent" th:text="${freeboard.frboardcontent}"></textarea>
<input type="submit" value ="글 수정">
</form>
</div>
<script>
document.addEventListener("DOMContentLoaded", function() {
CKEDITOR.replace('frboardcontent');
});
</script>
</body>
</html>
글작성부분과 동일하게 ckeditor을 사용했습니다.
📜게시글 조회 관련 로직 다시한번 살펴보기
1. 게시글 조회 요청처리
사용자가 글목록페이지에서 글제목링크를 클릭할때, FreeBoardController의 boardView 메서드가 출력됩니다.
@GetMapping( "/view" )
public String boardView( Model model , Integer id, FreeBoardEntity frbEntity) {
FreeBoardEntity frbTemp = frbService.freeBoardView(id);
if(frbTemp.getFrboardhits() == null) {
int hits = 0 ;
frbTemp.setFrboardhits( hits );
}else {
int hits = frbTemp.getFrboardhits() + 1 ;
frbTemp.setFrboardhits( hits );
}
frbService.FreeBoardWrite( frbTemp );
return "community/commfreeboardview";
}
boardView 메서드 는 id 매개변수를 통해 사용자가 조회하려는 게시글의 ID를 받습니다.
2. 게시글 조회 증가 로직
boardView 메서드내에서 frbService.freeBoardView(id)를 호출하여 데이터베이스에서 해당게시글을 조회합니다.
조회된 게시글의 조회수(frboardhits) 를 확인합니다.
조회수가 null이면 조회수를 0으로 설정하고, 그렇지않으면 현재 조회수에 1을 더해 조회수를 증가시킵니다.
3.업데이트된 게시글 저장
조회수가 업데이트된 FreeBoardEntity 객체는 다시 frbService.FreeBoardWrite(frbTemp)를 호출하여 데이터베이스에 저장됩니다. 여기서 FreeBoardWrite 메서드는 FreeBoardRepository의 save 메서드를 사용해 게시글을 업데이트합니다.
4. 사용자에게 게시글 표시
마지막으로 업데이트된 게시글 정보를 모델에 추가하고, 해당 게시글을 표시하는 뷰(community/commfreeboardview)로 리턴합니다.
view에는 이렇게 작성했습니다.
<span class="d-block text-sm" th:text="${freeboard.frboardhits} + ' 조회' "></span>