ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [게시판 만들기] 1️⃣ 설계와 CRUD 구현
    Spring Framework 2024. 5. 10. 18:08

    https://github.com/jngsngjn/board-study

    GitHub - jngsngjn/board-study: 게시판을 만들어 봅시다!

    게시판을 만들어 봅시다! Contribute to jngsngjn/board-study development by creating an account on GitHub.

    github.com

    목표

    • 게시판 CRUD 구현
    • Spring Security와 함께 인증, 인가 구현
    • 기본적인 검증 로직 구현
    • 회원가입 시 이메일 인증 구현

    개발 환경

    • 인텔리제이 ultimate
    • JDK 17
    • Gradle
    • Spring Boot
    • Thymeleaf, JPA, MySql, Bean Validation, Spring Security

    URL 설계

    • 게시글 페이지 조회 : /borads GET
    • 특정 게시글 조회 : /boards/{boardId} GET
    • 게시글 작성 : /boards/write GET POST
    • 게시글 수정 : /boards/{boardId}/edit GET PUT
    • 게시글 삭제 : /boards/{boardId}/delete DELETE

    인증, 인가

    • USER, ADMIN 권한 설정
    • 인증된 사용자(USER)만 글쓰기 가능
    • 비인증 사용자는 글 조회만 가능
    • 특정 게시글을 작성한 작성자 또는 관리자만이 글 삭제 가능

    CRUD 구현하기

    기본적인 게시판 CRUD 기능을 구현을 마친 후 기능을 점진적으로 발전시킬 계획이다.
     

    1. Create

    Controller

    @Controller
    @RequestMapping("/boards")
    public class BoardController {
    
        private final BoardService boardService;
    
        public BoardController(BoardService boardService) {
            this.boardService = boardService;
        }
        
        // 글쓰기 페이지 조회
        @GetMapping("/write")
        public String writeForm(Model model) {
            model.addAttribute("writeForm", new BoardForm());
            return "writeForm";
        }
        
        // 글쓰기
        @PostMapping("/write")
        public String write(@ModelAttribute BoardForm boardForm) {
            boardService.writeBoard(boardForm);
            return "redirect:/boards";
        }
    }

     
     
    Service

    @Service
    @Transactional
    public class BoardService {
    
        private final BoardRepository boardRepository;
    
        public BoardService(BoardRepository boardRepository) {
            this.boardRepository = boardRepository;
        }
        
        public void writeBoard(BoardForm boardForm) {
            Board board = new Board();
            board.setTitle(boardForm.getTitle());
            board.setAuthor(boardForm.getAuthor());
            board.setContent(boardForm.getContent());
    
            boardRepository.save(board);
        }
    }

     

    2. Read

     
    Controller

    @Controller
    @RequestMapping("/boards")
    public class BoardController {
    
        private final BoardService boardService;
    
        public BoardController(BoardService boardService) {
            this.boardService = boardService;
        }
    
        // 게시판 페이지 조회
        @GetMapping
        public String boardPage(@RequestParam(defaultValue = "1") int page, Model model) {
            model.addAttribute("boardList", boardService.findBoardList(page - 1));
            return "board";
        }
        
        // 특정 게시글 조회
        @GetMapping("/{boardId}")
        public String boardOne(@PathVariable Long boardId, Model model) {
            BoardOne board = boardService.findOneBoard(boardId);
    
            if (board == null) {
                return "rediect:/boards";
            }
    				
            // 조회수 증가
            boardService.viewBoardOne(boardId);
            model.addAttribute("board", board);
            return "boardOne";
        }
    }

     
     
    Service

    @Service
    @Transactional
    public class BoardService {
    
        private final BoardRepository boardRepository;
    
        public BoardService(BoardRepository boardRepository) {
            this.boardRepository = boardRepository;
        }
    	  
        // 모든 게시글 조회
        public Page<BoardList> findBoardList(int page) {
            Pageable pageable = PageRequest.of(page, 10);
            return boardRepository.findAllBy(pageable);
        }
    
        // 특정 게시글 조회
        public BoardOne findBoardOne(Long boardId) {
            return boardRepository.findByBoardId(boardId);
        }
    
        // 조회수 업데이트
        public void viewBoardOne(Long boardId) {
            boardRepository.incrementViewCount(boardId);
        }
    
    }

     
     
    게시판 페이지(board.html)

    <html lang="ko" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>게시판</title>
    </head>
    <body>
    <h2>게시판</h2>
    <hr>
    <form action="/boards/search" method="get">
        <input type="text" name="keyword" placeholder="검색어를 입력하세요">
        <button type="submit">검색</button>
    </form>
    <br>
    <table>
        <thead>
        <tr>
            <th>번호</th>
            <th>제목</th>
            <th>작성자</th>
            <th>작성일</th>
            <th>조회수</th>
        </tr>
        </thead>
    
        <tbody>
        <tr th:each="board : ${boardList}">
            <td th:text="${board.boardId}"></td>
            <td>
                <a th:href="@{/boards/{boardId}(boardId=${board.boardId})}" th:text="${board.title}"></a>
            </td>
            <td th:text="${board.author}"></td>
            <td th:text="${board.createdDate}"></td>
            <td th:text="${board.viewCount}"></td>
        </tr>
        </tbody>
    
    </table>
    <br>
    <div>
        <ul>
            <li th:if="${boardList.hasPrevious()}">
                <a th:href="@{/boards(page=${boardList.number})}">이전</a>
            </li>
            <li th:each="page : ${#numbers.sequence(1, boardList.totalPages)}" th:class="${page == boardList.number + 1} ? 'active'">
                <a th:href="@{/boards(page=${page})}" th:text="${page}"></a>
            </li>
            <li th:if="${boardList.hasNext()}">
                <a th:href="@{/boards(page=${boardList.number + 2})}">다음</a>
            </li>
        </ul>
    </div>
    <br>
    <a href="/boards/write">글쓰기</a>
    </body>
    </html>

     
     
    특정 게시글(boardOne.html)

    <html lang="ko" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>Document</title>
    </head>
    <body>
    
    <h2 th:text="${board.title}"></h2>
    <p>작성자: <span th:text="${board.author}"></span></p>
    <p>내용: <span th:text="${board.content}"></span></p>
    <p>작성일: <span th:text="${board.createdDate}"></span></p>
    <p>수정일: <span th:text="${board.modifiedDate}"></span></p>
    <p>조회수: <span th:text="${board.viewCount}"></span></p>
    <a th:href="@{/boards/{boardId}/edit(boardId=${board.boardId})}">수정</a>
    <form th:action="@{/boards/{boardId}/delete(boardId=${board.boardId})}" method="post">
        <input type="hidden" name="_method" value="delete" />
        <button type="submit">삭제</button>
    </form>
    <a href="/boards">뒤로 가기</a>
    </body>
    </html>

     

     

    3. Update

     
    Controller

    @Controller
    @RequestMapping("/boards")
    public class BoardController {
    
        private final BoardService boardService;
    
        public BoardController(BoardService boardService) {
            this.boardService = boardService;
        }
        
        // 수정 페이지 조회
        @GetMapping("/{boardId}/edit")
        public String editForm(@PathVariable Long boardId, Model model) {
            BoardOne board = boardService.findOneBoard(boardId);
            model.addAttribute("board", board);
            return "editForm";
        }
    
        // 글 수정
        @PostMapping("/{boardId}/edit")
        public String edit(@PathVariable Long boardId, @ModelAttribute BoardForm boardForm, RedirectAttributes redirectAttributes) {
            boardService.updateBoard(boardId, boardForm);
    
            redirectAttributes.addAttribute(boardId);
            return "redirect:/boards/{boardId}";
        }
    }

     
     
    Service

    @Service
    @Transactional
    public class BoardService {
    
        private final BoardRepository boardRepository;
    
        public BoardService(BoardRepository boardRepository) {
            this.boardRepository = boardRepository;
        }
        
        // 글 수정
        public void updateBoard(Long boardId, BoardForm boardForm) {
            Board board = boardRepository.findById(boardId)
                    .orElseThrow(() -> new RuntimeException("Board not found"));
    
            board.setTitle(boardForm.getTitle());
            board.setContent(boardForm.getContent());
            board.setAuthor(boardForm.getAuthor());
    
            boardRepository.save(board);
        }
    }

     

    4. Delete

     
    Controller

    @Controller
    @RequestMapping("/boards")
    public class BoardController {
    
        private final BoardService boardService;
    
        public BoardController(BoardService boardService) {
            this.boardService = boardService;
        }
        
        // 글 삭제
        @PostMapping("/{boardId}/delete")
        public String deleteOne(@PathVariable Long boardId) {
            boardService.delete(boardId);
            return "redirect:/boards";
        }
    }

     
     
    Service

    @Service
    @Transactional
    public class BoardService {
    
        private final BoardRepository boardRepository;
    
        public BoardService(BoardRepository boardRepository) {
            this.boardRepository = boardRepository;
        }
        
        // 글 삭제
        public void delete(Long boardId) {
            boardRepository.deleteById(boardId);
        }
    }

     


     
    기본적인 CRUD 작업이라 금방 구현할 것 같았지만 예상만큼 빨리 끝내지는 못했다. 페이징 기능을 처음 사용해 봐서 어색했지만 앞으로 사용할 일이 많을 것 같으니 얼른 익숙해져야겠다. URL 설계도 처음 해봐서 재미있었고 HTTP 메서드도 PUT, DELETE를 처음 사용해 봤다. 그리고 JPA 기술은 사용할 때마다 편리하고 신기함을 느끼는 것 같다. JPA 공부도 얼른 해야겠다! 이번 게시판 CRUD를 구현하면서 Controller, Service, Repository 간의 흐름에 대해 잘 정리할 수 있어서 좋았다!