Spring Example: Community #4 화면 제작 (Thymeleaf, Bootstrap)

함형주·2023년 1월 5일
0

질문, 피드백 등 모든 댓글 환영합니다.

유저에게 보여줄 화면을 Thymeleaf, Bootstrap을 이용하여 제작하겠습니다.

logout.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <link rel="stylesheet" type="text/css" href="/css/bootstrap.css">
</head>
<body>
<div class="col offset-10" th:fragment="logout">
    <form th:action="@{/logout}" method="post">
        <button type="button" class="mt-2 col-auto btn btn-light"
                th:onclick="|location.href='@{/post}'|">게시판</button>
        <button type="submit" class="mt-2 col-auto btn btn-light">로그아웃</button>
    </form>
</div>
</body>
</html>

로그인 시 상단에 표시될 레이아웃 조각입니다.

home

home.html

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
    <meta charset="utf-8">
    <link rel="stylesheet" type="text/css" href="/css/bootstrap.css">
</head>

<body>
<div sec:authorize="isAuthenticated()">
    <div th:replace="~{header/logout :: logout}"></div>
</div>
<div class="container position-absolute top-50 start-50 translate-middle">
    <hr class="mb-4">
    <h2 class="text-center">홈 화면</h2>
    <br>
    <div class="d-grid gap-2 col-6 mx-auto">
        <button class="w-100 btn btn-secondary btn-lg" sec:authorize="isAnonymous()"
                th:onclick="|location.href='@{/join}'|" type="button">
            회원 가입
        </button>
        <button class="w-100 btn btn-secondary btn-lg" sec:authorize="isAnonymous()"
                th:onclick="|location.href='@{/login}'|" type="button">
            로그인
        </button>

        <form th:action="@{/member/{member_id}(member_id=${member_id})}" th:method="delete">
            <button class="w-100 btn btn-secondary btn-lg" sec:authorize="isAuthenticated()" type="submit">
                회원 탈퇴
            </button>
        </form>
        <button class="w-100 btn btn-secondary btn-lg" sec:authorize="isAuthenticated()"
                th:onclick="|location.href='@{/post}'|" type="button">
            게시판
        </button>
    </div>
    <hr class="mt-4">
</div>
</body>
</html>

로그인 여부에 따라 logout.html을 렌더링하고 표시되는 버튼이 달라지도록 개발했습니다.

타임리프는 xmlns:sec="http://www.thymeleaf.org/extras/spring-security"를 추가하여 스프링 시큐리티에서 제공되는 기능을 사용할 수 있도록 지원합니다.

sec:authorize=""로 인증(로그인) 여부에 따라 해당 태그 렌더링을 결정할 수 있습니다. 인증이 완료된 상태라면 isAuthenticated()는 true를, isAnonymous()은 false를 반환합니다.

member

join.html

join.html

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="utf-8">
    <link rel="stylesheet" type="text/css" href="/css/bootstrap.css">
</head>
<body>
<div class="container position-absolute top-50 start-50 translate-middle">
    <hr class="mb-4">
    <h2 class="text-center">회원 가입</h2>
    <br>
    <form th:object="${memberDto}" method="post" th:action="@{/member}">
        <div class="d-grid gap-2 col-6 mx-auto">
            <label for="name" class="form-label">이름</label>
            <input type="text" id="name" class="w-100 form-control" th:field="*{name}">

            <label for="loginId" class="form-label">로그인 id</label>
            <input type="text" id="loginId" class="w-100 form-control" th:field="*{loginId}">

            <label for="password" class="form-label">비밀번호</label>
            <input type="password" id="password" class="w-100 form-control" th:field="*{password}">
        </div>

        <div class="m-5 row">
            <div class="col"></div>
            <div class="col-4">
                <button class="w-100 btn btn-success btn-lg" type="submit">회원 가입</button>
            </div>
            <div class="col-4">
                <button class="w-100 btn btn-outline-secondary btn-lg" type="button"
                        th:onclick="|location.href='@{/}'|">취소
                </button>
            </div>
            <div class="col"></div>
        </div>
    </form>
    <hr class="mt-4">
</div>
</body>
</html>

login.html

login.html

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="utf-8">
    <link rel="stylesheet" type="text/css" href="/css/bootstrap.css">
</head>

<body>
<div class="container position-absolute top-50 start-50 translate-middle">
    <hr class="mb-4">
    <h2 class="text-center">로그인</h2>
    <br>

    <form th:object="${loginDto}" method="post" th:action="@{/login}">
        <div class="d-grid gap-2 col-6 mx-auto">
            <div th:if="${param.error}" th:text="|ID 혹은 비밀번호가 잘못되었습니다.|"></div>
            <label for="loginId" class="form-label">로그인 ID</label>
            <input type="text" id="loginId" class="w-100 form-control" th:field="*{username}">

            <label for="password" class="form-label">비밀번호</label>
            <input type="password" id="password" class="w-100 form-control" th:field="*{password}">
        </div>

        <div class="m-4 row">
            <div class="col"></div>
            <div class="col-4">
                <button class="w-100 btn btn-success btn-lg" type="submit">로그인</button>
            </div>
            <div class="col-4">
                <button class="w-100 btn btn-outline-secondary btn-lg" type="button"
                        th:onclick="|location.href='@{/}'|">취소
                </button>
            </div>
            <div class="col"></div>
        </div>
    </form>
    <hr class="mt-4">
</div>
</body>
</html>

post

list.html

list.html

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="utf-8">
    <link rel="stylesheet" type="text/css" href="/css/bootstrap.css">
</head>
<body>
<div th:replace="~{header/logout :: logout}"></div>

<br>
<div class="px-5 gap-2 col mx-auto">
    <div class="d-grid gap-2 mx-auto">
        <button type="button" class="btn btn-primary btn-lg"
                th:onclick="|location.href='@{/post/add}'|">글쓰기
        </button>
        <br>
        <div class="row">
            <div class="col">
                <table class="table">
                    <thead>
                    <tr>
                        <th>작성자</th>
                        <th>제목 [댓글]</th>
                        <th>작성일</th>
                        <th>좋아요</th>
                    </tr>
                    </thead>

                    <tbody>
                    <tr th:each="post : ${postListDto}">
                        <td th:text="${post.membername}"></td>
                        <td>
                            <a th:href="@{/post/{post_id}(post_id=${post.id})}"
                               th:text="|${post.title} [${post.commentNum}]|"></a>
                        </td>
                        <td th:text="${post.createdDate}"></td>
                        <td th:text="${post.heartNum}"></td>
                    </tr>
                    </tbody>
                </table>
            </div>
        </div>
    </div>
</div>
</div>
</body>
</html>

단순히 Model에 담긴 postListDto를 출력합니다.

post.html

post.html

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="utf-8">
    <link rel="stylesheet" type="text/css" href="/css/bootstrap.css">
</head>
<body>
<div th:replace="~{header/logout :: logout}"></div>

<div class="container position-absolute top-50 start-50 translate-middle">

    <div class="d-grid gap-2 col-10 mx-auto" th:object="${postDto}">
        <div class="row">
            <h1 class="text-center" th:text="*{title}"></h1>
        </div>
        <div class="row">
            <div class="col" th:text="*{membername}"></div>
            <div class="col" th:text="' | ' + *{createdDate}"></div>
            <div class="col" th:text="' | ' + *{heartNum}"></div>
        </div>
        <div class="row">
            <hr class="mb-5">
        </div>
        <div class="row" th:if="${postDto.getMember_id().equals(guest_id)}">
            <div class="col-10"></div>
            <div class="col">
                <button type="button" th:onclick="|location.href='@{/post/edit/{post_id}(post_id=${post_id})}'|"
                        class="btn btn-link">수정
                </button>
            </div>
            <div class="col">
                <form th:action="@{/post/{post_id}(post_id=${post_id})}" th:method="delete">
                    <button type="submit" class="btn btn-link">삭제</button>
                </form>
            </div>
        </div>
        <div class="row">
            <div class="col-1"></div>
            <h4 class="col-8" th:text="*{body}"></h4>
            <div class="col-1"></div>
        </div>
        <div class="row">
            <div class="col-5"></div>
            <form th:action="@{/post/{post_id}/heart(post_id=${post_id})}" method="post">
                <div class="col-2">
                    <button type="submit" class="btn btn-outline-danger">좋아요</button>
                </div>
            </form>
            <div class="col-5"></div>

        </div>
        <div class="row">
            <hr class="mb-5 mt-5">
        </div>
        <div class="row">
            <fieldset>
                <form th:action="@{/post/{post_id}/comment(post_id=${post_id})}" method="post"
                      th:object="${commentDto}">
                    <legend class="text-center">댓글 작성</legend>
                    <br>
                    <div class="row">
                        <div class="col-11">
                            <textarea class="w-100 form-control" th:field="${commentDto.body}"></textarea>
                        </div>
                        <div class="col-1">
                            <div class="d-grid gap">
                                <button type="submit" class="btn btn-primary">추가</button>
                            </div>
                        </div>
                    </div>
                </form>
            </fieldset>
            <hr class="mb-5 mt-5">
            <div class="row">
                <div class="col">
                    <table class="table">
                        <tbody>
                        <tr th:each="comment : ${postDto.commentDtos}">
                            <td>
                                <div class="row">
                                    <div class="col-10">
                                        <p th:text="${comment.membername} + ' | ' + ${comment.body}"></p>
                                    </div>
                                    <div class="col" th:if="${comment.getMember_id().equals(guest_id)}">
                                        <button type="button"
                                                th:onclick="|location.href='@{/post/{post_id}/comment/edit/{comment_id}(post_id=${post_id}, comment_id=${comment.id})}'|"
                                                class="btn btn-link">수정
                                        </button>
                                    </div>
                                    <div class="col" th:if="${comment.getMember_id().equals(guest_id)}">
                                        <form th:action="@{/post/{post_id}/comment/{comment_id}(post_id=${post_id}, comment_id=${comment.id})}"
                                              th:method="delete">
                                            <button type="submit" class="btn btn-link">삭제</button>
                                        </form>
                                    </div>
                                </div>
                            </td>
                        </tr>
                        </tbody>
                    </table>
                </div>
            </div>
        </div>
    </div>
</div>
</div>
</body>
</html>

post.html 에선 게시글 단건 조회, 좋아요 버튼, 댓글 작성, 댓글 리스트 조회, *본인의 댓글일 시 수정/삭제 버튼 출력하는 로직을 포함합니다*.

model에 담긴 guest_id와 게시글, 댓글의 id와 비교하여 수정/삭제 버튼을 활성화합니다.

addform.html

addform.html

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="utf-8">
    <link rel="stylesheet" type="text/css" href="/css/bootstrap.css">
</head>
<body>
<div th:replace="~{header/logout :: logout}"></div>

<div class="container position-absolute top-50 start-50 translate-middle">
    <div class="d-grid gap-2 col-6 mx-auto">
        <form method="post" th:action="@{/post}" th:object="${postDto}">
            <fieldset>
                <legend class="text-center">게시글 작성</legend>
                <br>

                <label class="form-label" th:for="*{title}">제목</label>
                <input type="text" class="w-100 form-control" th:field="*{title}"><br>

                <label class="form-label" th:for="*{body}">내용</label>
                <textarea class="w-100 form-control" th:field="*{body}">
                </textarea>
                <br>

                <div class="row">
                    <div class="col-auto me-auto"></div>
                    <div class="col-auto">
                        <button type="submit" class="btn btn-primary">추가</button>
                    </div>
                    <div class="col-auto">
                        <button type="button" class="btn btn-primary" th:onclick="|location.href='@{/post}'|">취소
                        </button>
                    </div>
                </div>
            </fieldset>
        </form>
    </div>
</div>
</body>
</html>

editform.html

editfrom.html

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="utf-8">
    <link rel="stylesheet" type="text/css" href="/css/bootstrap.css">
</head>
<body>
<div th:replace="~{header/logout :: logout}"></div>

<div class="container position-absolute top-50 start-50 translate-middle">
    <div class="d-grid gap-2 col-6 mx-auto">
        <form th:action="@{/post/{post_id}(post_id=${post_id})}" th:object="${postDto}" th:method="put">
            <fieldset>
                <legend class="text-center">게시글 수정</legend>
                <br>

                <label class="form-label" th:for="*{title}">제목</label>
                <input type="text" class="w-100 form-control" th:field="*{title}"><br>

                <label class="form-label" th:for="*{body}">내용</label>
                <textarea class="w-100 form-control" th:field="*{body}"></textarea>
                <br>

                <div class="row">
                    <div class="col-auto me-auto"></div>
                    <div class="col-auto">
                        <button type="submit" class="btn btn-primary">수정</button>
                    </div>
                    <div class="col-auto">
                        <button type="button" class="btn btn-primary" th:onclick="|location.href='@{/post/{post_id}(post_id=${post_id})}'|">취소
                        </button>
                    </div>
                </div>
            </fieldset>
        </form>
    </div>
</div>
</body>
</html>

Comment

editform.html

editform.html

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="utf-8">
    <link rel="stylesheet" type="text/css" href="/css/bootstrap.css">
</head>
<body>
<div th:replace="~{header/logout :: logout}"></div>

<div class="container position-absolute top-50 start-50 translate-middle">
    <div class="d-grid gap-2 col-6 mx-auto">
        <form th:action="@{/post/{post_id}/comment/{comment_id}(post_id=${post_id}, comment_id=${comment_id})}"
              th:object="${commentDto}" th:method="put">
            <fieldset>
                <legend class="text-center">댓글 수정</legend>
                <br>

                <label class="form-label" th:for="*{body}">내용</label>
                <textarea class="w-100 form-control" th:field="*{body}"></textarea>
                <br>

                <div class="row">
                    <div class="col-auto me-auto"></div>
                    <div class="col-auto">
                        <button type="submit" class="btn btn-primary">수정</button>
                    </div>
                    <div class="col-auto">
                        <button type="button" class="btn btn-primary"
                                th:onclick="|location.href='@{/post/{post_id}(post_id=${post_id})}'|">취소
                        </button>
                    </div>
                </div>
            </fieldset>
        </form>
    </div>
</div>
</body>
</html>

다음으로

스프링 인터셉터를 이용하여 사용자가 악의적인 요청을 보낼 수 없도록하는 기능을 개발합니다. 요청을 보낸 회원의 id와 해당 엔티티의 member_id가 일치하는 경우에만 요청이 허락되도록 개발합니다.

github , 배포 URL (첫 접속 시 로딩이 걸릴 수 있습니다.)

profile
평범한 대학생의 공부 일기?

0개의 댓글