SpringBoot + Thymeleaf 게시판 프로젝트

devdo·2022년 3월 11일
0

SpringBoot

목록 보기
7/35
post-thumbnail

이번에는 Thymeleaf를 이용하여 SpringBoot 프로젝트 실습한 내용을 기록해봅니다.
페이징 처리하는데 Tymeleaf는 어떻게 처리하는지를 중점으로 정리해봤습니다.

Gradle, MySQL


Thymeleaf 설정 주의점

  • 기본적으로 파일경로는 resources/templates 바로 아래에 만들어서 사용한다.(설정가능)
  • Spring MVC를 쓰는 것 이기 때문에 @Controller로 사용한다.
  • cache에 넣어져 바로 적용이 안될 수 있어 applciation.yml에 spring.thymeleaf:cache: false해주어야 한다.

Backend : Springboot

build.gradle

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    // thymeleaf 설정
    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
    implementation 'org.thymeleaf.extras:thymeleaf-extras-java8time'

    compileOnly 'org.projectlombok:lombok'
    developmentOnly 'org.springframework.boot:spring-boot-devtools'
    runtimeOnly 'com.h2database:h2'
    runtimeOnly 'mysql:mysql-connector-java'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

application.yml

server:
  port: 8090
## cache가 되어 있으면 제대로 안나오는 경우가 있어 false 처리  
spring:
  thymeleaf:
    cache: false

  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/{database}
    username: root
    password: {root password}

  jpa:
    open-in-view: true
    hibernate:
      ddl-auto: update
      naming:
        physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
      use-new-id-generator-mappings: false
    show-sql: true
    properties:
      hibernate.format_sql: true
      dialect: org.hibernate.dialect.MySQL5InnoDBDialect


logging:
  level:
    org.hibernate.SQL: debug

Board

@Entity
@Getter
@Setter
@ToString
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "board")
public class Board {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String title;

    private String content;
}

BoardRepository

public interface BoardRepository extends JpaRepository<Board, Long> {

	// 검색 키워드
    Page<Board> findByTitleContaining(String searchKeyword, Pageable pageable);
}

BoardService

@Service
@RequiredArgsConstructor
public class BoardService {

    private final BoardRepository boardRepository;

    // 글 작성 처리
    public void write(Board board) {
        boardRepository.save(board);
    }

    // 게시글 리스트 처리
    public Page<Board> list(Pageable pageable) {
        return boardRepository.findAll(pageable);
    }

    // 특정 게시글 상세보기
    public Board view(Long id) {
        return boardRepository.findById(id)
                .orElseThrow(() -> new RuntimeException("없는 id입니다."));
    }

    // 특정 게시글 삭제
    public void deleteById(Long id) {
        boardRepository.deleteById(id);
    }

    public Page<Board> searchList(String searchKeyword, Pageable pageable) {
        return boardRepository.findByTitleContaining(searchKeyword, pageable);
    }
}

BoardController

// 주의! : Pageble -> import org.springframework.data.domain.Pageable;
@Slf4j
@Controller
@RequestMapping("/board")
@RequiredArgsConstructor
public class BoardController {

    private final BoardService boardService;

    @GetMapping("/write")
    public String write() {
        return "board/write";
    }


    @PostMapping("/writedo")
    public String writedo(Board board, Model model) {

        boardService.write(board);

        model.addAttribute("message", "글 작성이 완료되었습니다.");
        model.addAttribute("searchUrl", "/board/list");

        return "board/message";
    }

    @GetMapping("/list")
    public String list(Model model,
                       @PageableDefault(page = 0, size = 10, sort = "id", direction = Sort.Direction.DESC) Pageable pageable,
                       String searchKeyword
    ) {
        Page<Board> list = null;

        if (searchKeyword == null) {
            list = boardService.list(pageable);
        } else {
            list = boardService.searchList(searchKeyword, pageable);
        }

        int nowPage = list.getPageable().getPageNumber() + 1;
        int startPage = Math.max(nowPage - 4, 1);
        int endPage = Math.min(nowPage + 5, list.getTotalPages());

        model.addAttribute("list", list);
        model.addAttribute("nowPage", nowPage);
        model.addAttribute("startPage", startPage);
        model.addAttribute("endPage", endPage);

        return "board/list";
    }

    @GetMapping("/view/{id}")
    public String view(Model model, @PathVariable("id") Long id) {
        model.addAttribute("board", boardService.view(id));
        return "board/view";
    }

    @GetMapping("/delete/{id}")
    public String delete(@PathVariable("id") Long id) {
        boardService.deleteById(id);

        return "redirect:/board/list";
    }

    @GetMapping("/modify/{id}")
    public String modify(@PathVariable("id") Long id,
                         Model model) {
        model.addAttribute("board", boardService.view(id));

        return "board/modify";
    }

    @PostMapping("/update/{id}")
    public String update(@PathVariable("id") Long id, Board board) {

        Board boardTemp = boardService.view(id);
        boardTemp.setTitle(board.getTitle());
        boardTemp.setContent(board.getContent());

        boardService.write(boardTemp);

        return "redirect:/board/list";
    }
}

TestCode

게시판 dumy 데이터는 TestCode로 넣었습니다.

@SpringBootTest
class BoardRepositoryTest {

    @Autowired
    BoardRepository boardRepository;

    @BeforeEach
    void beforeEach() {
        boardRepository.deleteAll();
    }

    // Long id도 초기화 -> application.yml 내 ddl-auto: create

    @DisplayName("add 테스트")
    @Test
    public void addTest() {

        IntStream.rangeClosed(1,140).forEach(i -> {
            Board board = Board.builder()
                    .title("board_dsg_title" + i)
                    .content("board_dsg_content" + i)
                    .build();
            boardRepository.save(board);
        });

    }

}

Frontend : Tymeleaf

board/list.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>게시물 리스트</title>
</head>
<style>
    .layout {
        width: 500px;
        margin: 0 auto;
        margin-top: 40px;
    }

    .layout input {
        width: 100%;
        box-sizing: border-box;
    }

    .layout textarea {
        width: 100%;
        margin-top: 10px;
        min-height: 300px;
    }
</style>
<body>
    <div class="layout">
        <table>
            <thead>
                <tr>
                    <th>글번호</th>
                    <th>제목</th>
                </tr>
            </thead>
            <tbody>
                <tr th:each="board : ${list}">
                    <td th:text="${board.id}"></td>
                    <td>
                        <a th:text="${board.title}" th:href="@{/board/view/{id}(id=${board.id})}"></a>
                    </td>
                </tr>
            </tbody>
        </table>

        <th:block th:each="page : ${#numbers.sequence(startPage, endPage)}">
            <a th:if="${page != nowPage}" th:href="@{/board/list(page = ${page - 1}, searchKeyword = ${param.searchKeyword})}" th:text="${page}"></a>
            <strong th:if="${page == nowPage}" th:text="${page}" style="color: red"></strong>
        </th:block>
        <form th:action="@{/board/list}" method="get">
            <input style="width: 130px;" type="text" name="searchKeyword">
            <button type="submit">검색</button>
        </form>
    </div>

</body>
</html>

board/write.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>게시물 작성폼</title>
</head>
<style>
    .layout {
        width: 500px;
        margin: 0 auto;
    }

    .layout input {
        width: 100%;
        box-sizing: border-box;
    }

    .layout textarea {
        width: 100%;
        margin-top: 10px;
        min-height: 300px;
    }
</style>
<body>
    <div class="layout">
        <form action="/board/writedo" method="post">
            <input name="title" type="text"> <br>
            <textarea name="content"></textarea> <br>
            <button type="submit">작성</button>        
        </form>

    </div>

</body>
</html>

board/view.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>게시물 상세 페이지</title>
</head>
<style>
    .layout {
        width: 500px;
        margin: 0 auto;
        margin-top: 40px;
    }

    .layout input {
        width: 100%;
        box-sizing: border-box;
    }

    .layout textarea {
        width: 100%;
        margin-top: 10px;
        min-height: 300px;
    }
</style>
<body>
    <h1 th:text="${board.title}">제목입니다.</h1>
    <p th:text="${board.content}">내용이 들어간 부분입니다.</p>
    <a th:href="@{/board/delete/{id}(id=${board.id})}">글 삭제</a>
    <a th:href="@{/board/modify/{id}(id=${board.id})}">글 수정</a>
</body>
</html>

board/modfiy.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>게시물 수정폼</title>
</head>
<style>
    .layout {
        width: 500px;
        margin: 0 auto;
    }

    .layout input {
        width: 100%;
        box-sizing: border-box;
    }

    .layout textarea {
        width: 100%;
        margin-top: 10px;
        min-height: 300px;
    }
</style>
<body>
    <div class="layout">
        <form th:action="@{/board/update/{id}(id = ${board.id})}" method="post">
            <input name="title" type="text" th:value="${board.title}"> <br>
            <textarea name="content" th:text="${board.content}"></textarea> <br>
            <button type="submit">수정</button>
        </form>

    </div>

</body>
</html>

board/message.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>message</title>
</head>

<script th:inline="javascript">

    var message = [[${message}]];
    alert(message);

    location.replace([[${searchUrl}]]);

</script>

<body>

</body>
</html>


참고

소스출처 : https://github.com/mooh2jj/springboot-board-example.git

profile
배운 것을 기록합니다.

0개의 댓글