본문 바로가기
풀스택 개발 학습 과정/백엔드(Java, Spring)

[스프링] 게시판 구현 - 1 (게시글 목록 요청)

by 육츠 2024. 3. 18.

구현은 완료된 상태. 교육 수업 시간에 교수님과 함께 만든 것이기 때문에 수업들었던 흐름을 생각하며 다시 되짚어 보려고 한다.

1.  첫 요청 MainController

인증 여부에 따라 각 메뉴별 출력을 다르게 함 (우선은 칸만 보이게 한다.)

package net.kdigital.board.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class MainController {
	
	@GetMapping({"/", ""})
	public String index() {
		return "index";
	}
}

 

return "index"; 이므로 index.html 파일 필요

<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>게시판</title>

    <!-- CSS 설정 -->
    <link rel="stylesheet" th:href="@{/css/main.css}">
</head>

<body>
    <div class="container">
        <div class="logo">
            <img th:src="@{/images/logo.png}" alt="logo">
            <h2>회원 전용 게시판</h2>
        </div>

        <div class="gnb">
            <ul>
                <!-- 인증이 되지 않은 사람 -->
                <li><a th:href="@{/}">회원가입</a></li>
                <li><a th:href="@{/}">로그인</a></li>

                <!-- 인증이 된 사람 -->
                <li><a th:href="@{/}">로그아웃</a></li>

                <!-- 인증과 관계없음 -->
                <li><a th:href="@{/board/boardList}">게시판</a></li>
            </ul>
        </div>
    </div> <!-- div.container 끝 -->
</body>

</html>

이 부분은 미리 css 적용까지 마무리 되어있기 때문에 해당 사진 처럼 나오는 것 (원래는 왼쪽 사이드에 검은 글씨로만 쓰여져 있음)

게시판

 

2. BoardController

2-1) 게시글 목록 요청

페이징처리, 검색기능(글쓴이, 제목, 내용)

@Slf4j
@Controller
@RequestMapping("/board")
public class BoardController {
	private BoardService boardService;
	
	// 생성자 초기화
	public BoardController(BoardService boardService) {
		this.boardService = boardService;
	}
	
	// 파일의 저장경로
	@Value("${spring.servlet.multipart.location}")
	String uploadPath;

 

boardList.html에서 form 태그의 Metod="GET" 일때 글 목록을 요청

/**
	 * 글 목록 요청
	 * 1) index에서 넘어올 경우 searchItem과 searchWord가 없다
	 * 2) 목록에서 검색하여 넘어올 경우 earchItem과 searchWord가 있다
	 * @return
	 */
	@GetMapping("/boardList")
	public String boardList(
			@RequestParam(name="searchItem", defaultValue="boardTitle") String searchItem
			, @RequestParam(name="searchWord", defaultValue="") String searchWord
			, Model model) {
		
		List<BoardDTO> dtoList = boardService.selectAll(searchItem, searchWord);
		
		model.addAttribute("list", dtoList);
		model.addAttribute("searchItem", searchItem);
		model.addAttribute("searchWord", searchWord);
		
		return "board/boardList";
	}

BoardService에서 selectAll 이라는 메소드를 통해 리스트를 반환한다.  형태는 List로 오기 때문에 dtoList로 받는다.

* 게시글을 클릭하여 다시 돌아와도 검색한 단어와 제목/작성자/내용 중 선택한 값이 남아있도록 설정되어있다.

boardList.html 전체 코드

더보기
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>게시글 목록</title>

    <!-- CSS 설정 -->
    <link rel="stylesheet" th:href="@{/css/list.css}">
</head>

<body>
    <div class="container">
        <div class="logo">
            <a th:href="@{/}"><img th:src="@{/images/logo.png}" alt="logo"></a>
            <h2>게시글 목록</h2>
        </div>
    </div> <!-- div.container 끝 -->

    <!-- 게시글 전체 목록 출력-->
    <div class="content">
        <p th:if="${#lists.isEmpty(list)}">목록이 없습니다.</p>
        <div class="head">
            <!-- 전체 글 개수 출력 -->
            <div class="count">
                <p>게시글 개수 : <span>0</span></p>
            </div>

            <!-- 검색 창 -->
            <div class="search">
                <form th:action="@{/board/boardList}">
                    <select name="searchItem">
                        <option value="boardTitle"   th:selected="${searchItem == 'boardTitle'}">글제목</option>
                        <option value="boardWriter"  th:selected="${searchItem == 'boardWriter'}">작성자</option>
                        <option value="boardContent" th:selected="${searchItem == 'boardContent'}">글내용</option>
                    </select>
                    <input type="text" name="searchWord" th:value="${searchWord}">
                    <input type="submit" value="검색" class="btn btn-light">
                </form>
            </div>
        </div>
        <!-- 게시글 목록 테이블 -->
        <div>
            <table border="1">
                <tr>
                    <th class="no">글번호</th>
                    <th>제목</th>
                    <th>작성자</th>
                    <th>조회수</th>
                    <th>작성일</th>
                </tr>
                <!-- 반복 구간 -->
                <tr th:each="board, status : ${list}">
                    <td th:text="${status.count}"></td>
                    <td>
                        <a th:href="@{/board/boardDetail(boardNum=${board.boardNum}, searchItem=${searchItem}, searchWord=${searchWord})}" th:text="${board.boardTitle}"></a>
                        <span th:if="${board.originalFileName != null}">
                            <img th:src="@{/images/attachment.png}" alt="첨부파일" width="17px">
                        </span>
                    </td>
                    <td th:text="${board.boardWriter}">홍길동</td>
                    <td th:text="${board.hitCount}">22</td>
                    <td th:text="*{#temporals.format(board.createDate, 'yyyy-MM-dd hh:mm:ss')}">2024-03-12</td>
                </tr>
            </table>
        </div> <!-- div.content 끝 -->

        <!-- 글쓰기 버튼 -->
        <div class="write">
            <a th:href="@{/board/boardWrite}" class="btn btn-light">글쓰기</a>
        </div>

        <!-- Page Navigation -->
        <nav class="pagination">
            First Prev

            <a href="#">1</a>

            2 3 4 5 Next Last
        </nav>
    </div>
</body>

</html>

 

    <!-- 게시글 전체 목록 출력-->
    <div class="content">
        <p th:if="${#lists.isEmpty(list)}">목록이 없습니다.</p>

list 안에 내용이 없다면 목록이 없습니다. 를 화면에 보이게 했다.

 

만약 게시글이 있다면 전체 글 개수를 출력하고 글제목과 내용 그리고 작성자가 보이게 해 두었다.

        <div class="head">
            <!-- 전체 글 개수 출력 -->
            <div class="count">
                <p>게시글 개수 : <span>0</span></p>
            </div>

            <!-- 검색 창 -->
            <div class="search">
                <form th:action="@{/board/boardList}">
                    <select name="searchItem">
                        <option value="boardTitle"   th:selected="${searchItem == 'boardTitle'}">글제목</option>
                        <option value="boardWriter"  th:selected="${searchItem == 'boardWriter'}">작성자</option>
                        <option value="boardContent" th:selected="${searchItem == 'boardContent'}">글내용</option>
                    </select>
                    <input type="text" name="searchWord" th:value="${searchWord}">
                    <input type="submit" value="검색" class="btn btn-light">
                </form>
            </div>
        </div>
코드 내용
<option value="boardTitle"   th:selected="${searchItem == 'boardTitle'}">글제목</option> 찾으려는 단어와 같으면 선택되게 하였다.

 

 	<!-- 글쓰기 버튼 -->
        <div class="write">
            <a th:href="@{/board/boardWrite}" class="btn btn-light">글쓰기</a>
        </div>

글쓰기 버튼을 누르면 boardWrite로 가도록 설정

 

3. BoardEntity

전체 코드

더보기
package net.kdigital.board.entity;

import java.time.LocalDateTime;

import org.hibernate.annotations.CreationTimestamp;
import org.springframework.data.annotation.LastModifiedDate;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.SequenceGenerator;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import net.kdigital.board.dto.BoardDTO;

@NoArgsConstructor
@AllArgsConstructor
@Setter
@Getter
@ToString
@Builder

@Entity
@Table(name = "board")
public class BoardEntity {
	@SequenceGenerator(
		name = "board_seq"
		, sequenceName = "board_seq"
		, initialValue = 1
		, allocationSize = 1
	)
	
	@Id
	@GeneratedValue(generator = "board_seq")
	@Column(name = "board_num")
	private Long boardNum;
	
	@Column(name = "board_writer", nullable = false)
	private String boardWriter;
	
	@Column(name = "board_title")
	private String boardTitle;
	
	@Column(name = "board_content")
	private String boardContent;
	
	@Column(name = "hit_count")
	private int hitCount;
	
	@Column(name = "favorite_count")
	private int favoriteCount;
	
	@Column(name = "create_date")
	@CreationTimestamp		// 게시글이 처음 생성될 때 자동으로 날짜를 세팅
	private LocalDateTime createDate;
	
	@Column(name = "update_date")
	@LastModifiedDate		// 게시글이 수정된 마지막 날짜/시간을 세팅
	private LocalDateTime updateDate;
	
	// 첨부 파일이 있을 때
	@Column(name = "original_file_name")
	private String originalFileName;	// 원본 파일의 파일명
	
	@Column(name = "saved_file_name")
	private String savedFileName;		// 하드디스크에 저장될 파일명
	
	
	// DTO --> Entity 반환
	public static BoardEntity toEntity(BoardDTO boardDTO) {
		return BoardEntity.builder()
				.boardNum(boardDTO.getBoardNum())
				.boardWriter(boardDTO.getBoardWriter())
				.boardTitle(boardDTO.getBoardTitle())
				.boardContent(boardDTO.getBoardContent())
				.hitCount(boardDTO.getHitCount())
				.favoriteCount(boardDTO.getFavoriteCount())
				.originalFileName(boardDTO.getOriginalFileName())
				.savedFileName(boardDTO.getSavedFileName())
				.build();
	}
}

데이터 베이스와 직접 연결 된다. (대소문자 주의, 이름 주의)

 

 

4. BoardDTO

직접적으로 데이터 베이스와 연결하는 부분이다.

전체 코드

더보기
package net.kdigital.board.dto;

import java.time.LocalDateTime;

import org.springframework.web.multipart.MultipartFile;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import net.kdigital.board.entity.BoardEntity;

@NoArgsConstructor
@AllArgsConstructor
@Setter
@Getter
@ToString
@Builder
public class BoardDTO {
	private Long boardNum;
	private String boardWriter;
	private String boardTitle;
	private String boardContent;
	private int hitCount;
	private int favoriteCount;
	private LocalDateTime createDate;
	private LocalDateTime updateDate;
	
	// 파일이 첨부되었을 때 추가작업
	private MultipartFile uploadFile;
	private String originalFileName;	// 원본 파일의 파일명
	private String savedFileName;		// 하드디스크에 저장될 파일명
	
	// Entity --> DTO 반환
	public static BoardDTO toDTO(BoardEntity boardEntity) {
		return BoardDTO.builder()
				.boardNum(boardEntity.getBoardNum())
				.boardWriter(boardEntity.getBoardWriter())
				.boardTitle(boardEntity.getBoardTitle())
				.boardContent(boardEntity.getBoardContent())
				.hitCount(boardEntity.getHitCount())
				.favoriteCount(boardEntity.getFavoriteCount())
				.createDate(boardEntity.getCreateDate())
				.updateDate(boardEntity.getUpdateDate())
				.originalFileName(boardEntity.getOriginalFileName())
				.savedFileName(boardEntity.getSavedFileName())
				.build();
	}
}

 

* CSS

<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>게시글 목록</title>

    <!-- CSS 설정 -->
    <link rel="stylesheet" th:href="@{/css/list.css}">
</head>

main.css 는 전체 페이지를 아우르는 css 로 활용될 것이다.

코드의 깔끔함을 위해 @import 하여 적용시킨다.