질문, 피드백 등 모든 댓글 환영합니다.
유저에게 보여줄 화면을 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.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를 반환합니다.
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
<!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>
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
<!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
<!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>
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>
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가 일치하는 경우에만 요청이 허락되도록 개발합니다.