Contents
1. 테이블 세팅1) Reply.java2) data.sql - 더미데이터 추가2. 댓글 보기1) ReplyResponse.java2) BoardResponse.java3) BoardController.java4) detail.mustache4. 댓글 쓰기1) ReplyRequest.java2) ReplyController.java3) ReplyService.java4) ReplyRepository.java5. 댓글 삭제1) ReplyController.java2) ReplyService.java3) ReplyRepository.java4) detail.mustache※ 로그인 안 한 상태에서 댓글 입력창 클릭했을 때1) detail.mustache - 수정1. 테이블 세팅
1) Reply.java
C:\workspace\spring_lab\boardv1\src\main\java\com\example\boardv1\reply\Reply.java
package com.example.boardv1.reply;
import java.time.LocalDateTime;
import org.hibernate.annotations.CreationTimestamp;
import com.example.boardv1.board.Board;
import com.example.boardv1.user.User;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 게시글 1 : 댓글 N
* 유저 1 : 댓글 N
*/
@NoArgsConstructor
@Data
@Entity
@Table(name = "reply_tb")
public class Reply {
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Id
private Integer id;
private String comment;
@ManyToOne // 디폴트 : EAGER 전략
private Board board; // board_tb
@ManyToOne
private User user;
@CreationTimestamp
private LocalDateTime createdAt;
}2) data.sql - 더미데이터 추가
C:\workspace\spring_lab\boardv1\src\main\resources\db\data.sql
insert into reply_tb (user_id, board_id, comment, created_at) values(1, 6, 'comment1', now());
insert into reply_tb (user_id, board_id, comment, created_at) values(1, 6, 'comment2', now());
insert into reply_tb (user_id, board_id, comment, created_at) values(2, 6, 'comment3', now());
insert into reply_tb (user_id, board_id, comment, created_at) values(1, 5, 'comment4', now());
insert into reply_tb (user_id, board_id, comment, created_at) values(2, 5, 'comment5', now());2. 댓글 보기
1) ReplyResponse.java
C:\workspace\spring_lab\boardv1\src\main\java\com\example\boardv1\reply\ReplyResponse.java
package com.example.boardv1.reply;
public class ReplyResponse {
public static class DTO {
private Integer id;
private String comment;
private Integer replyUserId;
private String replyUsername;
private boolean isReplyOwner; // 로그인한 유저가 댓글을 작성한 유저인지?
public DTO(Reply reply, Integer sessionUserId) {
this.id = reply.getId();
this.comment = reply.getComment();
this.replyUserId = reply.getUser().getId();
this.replyUsername = reply.getUser().getUsername();
this.isReplyOwner = reply.getUser().getId() == sessionUserId;
}
}
}2) BoardResponse.java
C:\workspace\spring_lab\boardv1\src\main\java\com\example\boardv1\board\BoardResponse.java
package com.example.boardv1.board;
import java.util.List;
import com.example.boardv1.reply.ReplyResponse;
import lombok.Data;
public class BoardResponse {
@Data
public static class DetailDTO {
// 화면에 보이지 않는 것 (PK는 화면에 안보여도 무조건 적어야 함!)
private int id;
private int userId;
// 화면에 보이는 것
private String title;
private String content;
private String username;
// 연산해서 만들어야 되는 것
private boolean isOwner; // 게시글의 주인인가?
private List<ReplyResponse.DTO> replies;
public DetailDTO(Board board, Integer sessionUserId) {
this.id = board.getId();
this.userId = board.getUser().getId();
this.title = board.getTitle();
this.content = board.getContent();
this.username = board.getUser().getUsername();
this.isOwner = board.getUser().getId() == sessionUserId;
this.replies = board.getReplies().stream()
.map(reply -> new ReplyResponse.DTO(reply, sessionUserId))
.toList();
}
}
}3) BoardController.java
package com.example.boardv1.board;
import java.util.List;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import com.example.boardv1.user.User;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor // final이 적혀있는 애로 생성자 만들어줌
@Controller // @Controller 적어야 리턴값이 파일이 됨 (외부진입점)
public class BoardController {
private final BoardService boardService;
private final HttpSession session;
// title : title=title7&content=content7 (x-www-form)
@PostMapping("/boards/save")
public String save(BoardRequest.SaveOrUpdateDTO reqDTO){ // new해서 넣어줌! 필드가 많을 때 상태만 추가하면 되기 때문에 매우 편함! 재사용 가능!(필드명 잘 적어야 함)
// 인증(v) 권한(x)
// 인증
User sessionUser = (User) session.getAttribute("sessionUser");
if (sessionUser == null)
throw new RuntimeException("인증되지 않았습니다.");
boardService.게시글쓰기(reqDTO.getTitle(), reqDTO.getContent());
return "redirect:/";
}
// body : title=제목&content=내용
@PostMapping("/boards/{id}/update")
public String update(@PathVariable("id") int id, BoardRequest.SaveOrUpdateDTO reqDTO){
// 인증(v) 권한(v)
// 인증
User sessionUser = (User) session.getAttribute("sessionUser");
if (sessionUser == null)
throw new RuntimeException("인증되지 않았습니다.");
boardService.게시글수정(id,reqDTO.getTitle(), reqDTO.getContent(), sessionUser.getId());
return "redirect:/boards/"+id;
}
@GetMapping("/")
public String index(HttpServletRequest req){
List<Board> list = boardService.게시글목록();
req.setAttribute("models", list);
return "index";
}
@GetMapping("/boards/save-form")
public String saveForm(){
// 인증(v) 권한(x)
// 인증
User sessionUser = (User) session.getAttribute("sessionUser");
if (sessionUser == null)
throw new RuntimeException("인증되지 않았습니다.");
return "board/save-form";
}
@GetMapping("/boards/{id}/update-form")
public String updateForm(@PathVariable("id") int id, HttpServletRequest req){
// 인증(v) 권한(v)
// 인증
User sessionUser = (User) session.getAttribute("sessionUser");
if (sessionUser == null)
throw new RuntimeException("인증되지 않았습니다.");
Board board = boardService.수정폼게시글정보(id, sessionUser.getId());
req.setAttribute("model", board);
return "board/update-form";
}
@GetMapping("/boards/{id}")
public String detail(@PathVariable("id") int id, HttpServletRequest req){
User sessionUser = (User) session.getAttribute("sessionUser"); // Object타입으로 반환하기 때문에 다운캐스팅해야 함
Integer sessionUserId = sessionUser == null ? null : sessionUser.getId();
BoardResponse.DetailDTO dto = boardService.상세보기(id, sessionUserId);
req.setAttribute("model", dto);
return "board/detail"; // mustache 파일의 경로
}
@PostMapping("/boards/{id}/delete")
public String delete(@PathVariable("id") int id){
// 인증(v) 권한(v)
// 인증
User sessionUser = (User) session.getAttribute("sessionUser");
if (sessionUser == null)
throw new RuntimeException("인증되지 않았습니다.");
boardService.게시글삭제(id, sessionUser.getId());
return "redirect:/";
}
@GetMapping("/api/boards/{id}")
public @ResponseBody BoardResponse.DetailDTO apiDetail(@PathVariable("id") int id) {
User sessionUser = (User) session.getAttribute("sessionUser");
Integer sessionUserId = sessionUser == null ? null : sessionUser.getId();
BoardResponse.DetailDTO dto = boardService.상세보기(id, sessionUserId);
return dto;
}
}
4) detail.mustache
{{> header}}
<div class="container p-5">
{{#model.isOwner}}
<!-- 수정삭제버튼 -->
<div class="d-flex justify-content-end">
<a href = "/boards/{{model.id}}/update-form" class="btn btn-secondary me-1">수정</a>
<form action = "/boards/{{model.id}}/delete" method="post">
<button class="btn btn-outline-secondary">삭제</button>
</form>
</div>
{{/model.isOwner}}
<!-- 게시글내용 -->
<div>
<h2><b>{{model.title}}</b></h2>
<hr />
<div class="d-flex justify-content-end">
작성자 : {{model.username}}
</div>
<div class="m-4 p-2">
{{model.content}}
</div>
</div>
<!-- 댓글 -->
<div class="card mt-3">
<!-- 댓글등록 -->
<div class="card-body">
<form action="/replies/save" method="post">
<textarea class="form-control" rows="2" name="comment"></textarea>
<div class="d-flex justify-content-end">
<button class="btn btn-secondary mt-1">
댓글등록
</button>
</div>
</form>
</div>
<!-- 댓글목록 -->
<div class="card-footer">
<b>댓글리스트</b>
</div>
<div class="list-group">
{{#model.replies}}
<!-- 댓글아이템 -->
<div class="list-group-item d-flex justify-content-between align-items-center">
<div class="d-flex">
<div class="px-1 me-1 bg-secondary text-white rounded">{{replyUsername}}</div>
<div>{{comment}}</div>
</div>
{{#isReplyOwner}}
<form action="/replies/{{id}}/delete" method="post">
<button class="btn">🗑</button>
</form>
{{/isReplyOwner}}
</div>
{{/model.replies}}
</div>
</div>
</div>
</body>
</html>DTO 만드는 이유(목적)
- 추가 필드가 필요할 때
- 필드를 원하는 것만 뽑아서 전달하고 싶을 때
- 화면에 딱 맞는 데이터를 전달하기 위해
4. 댓글 쓰기
1) ReplyRequest.java
package com.example.boardv1.reply;
import lombok.Data;
public class ReplyRequest {
@Data
public static class SaveDTO {
private Integer boardId;
private String comment;
}
}2) ReplyController.java
package com.example.boardv1.reply;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import com.example.boardv1.user.User;
import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Controller
public class ReplyController {
private final ReplyService replyService;
private final HttpSession session;
@PostMapping("/replies/save")
public String save(ReplyRequest.SaveDTO reqDTO){
// 인증
User sessionUser = (User) session.getAttribute("sessionUser");
if (sessionUser == null)
throw new RuntimeException("인증되지 않았습니다.");
int sessionUserId = sessionUser.getId();
replyService.댓글등록(sessionUserId, reqDTO.getBoardId(), reqDTO.getComment());
return "redirect:/boards/" + reqDTO.getBoardId();
}
}3) ReplyService.java
package com.example.boardv1.reply;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.example.boardv1.board.Board;
import com.example.boardv1.board.BoardRepository;
import com.example.boardv1.user.User;
import com.example.boardv1.user.UserRepository;
import jakarta.persistence.EntityManager;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Service
public class ReplyService {
private final ReplyRepository replyRepository;
private final EntityManager em;
@Transactional
public void 댓글등록(int sessionUserId, int boardId, String comment) {
Board board = em.getReference(Board.class, boardId); // 가짜로 영속화. 조회하면 계속 쿼리 터지는데 조회 안해도 됨
User user = em.getReference(User.class, sessionUserId);
Reply reply = new Reply();
reply.setBoard(board);
reply.setUser(user);
reply.setComment(comment);
replyRepository.save(reply);
}
}4) ReplyRepository.java
package com.example.boardv1.reply;
import java.util.Optional;
import org.springframework.stereotype.Repository;
import jakarta.persistence.EntityManager;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Repository
public class ReplyRepository {
private final EntityManager em;
public Reply save(Reply reply) {
em.persist(reply);
return reply;
}
}5. 댓글 삭제
1) ReplyController.java
package com.example.boardv1.reply;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import com.example.boardv1.user.User;
import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Controller
public class ReplyController {
private final ReplyService replyService;
private final HttpSession session;
// replies/5/delete?boardId=2
@PostMapping("/replies/{id}/delete")
public String delete(@PathVariable("id") int id, @RequestParam("boardId") int boardId){
// 인증
User sessionUser = (User) session.getAttribute("sessionUser");
if (sessionUser == null)
throw new RuntimeException("인증되지 않았습니다.");
replyService.댓글삭제(id, sessionUser.getId());
return "redirect:/boards/" + boardId;
}
}
2) ReplyService.java
package com.example.boardv1.reply;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.example.boardv1.board.Board;
import com.example.boardv1.board.BoardRepository;
import com.example.boardv1.user.User;
import com.example.boardv1.user.UserRepository;
import jakarta.persistence.EntityManager;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Service
public class ReplyService {
private final ReplyRepository replyRepository;
private final EntityManager em;
@Transactional
public void 댓글삭제(int id, int sessionUserId) {
// 영속화(권한체크 하려면 findById해야 함. 권한체크 안해도 되면 getReference사용)
Reply reply = replyRepository.findById(id)
.orElseThrow(() -> new RuntimeException("삭제할 댓글을 찾을 수 없어요"));
// 권한
if (sessionUserId != reply.getUser().getId())
throw new RuntimeException("삭제할 권한이 없습니다");
replyRepository.delete(reply);
}
}
3) ReplyRepository.java
package com.example.boardv1.reply;
import java.util.Optional;
import org.springframework.stereotype.Repository;
import jakarta.persistence.EntityManager;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Repository
public class ReplyRepository {
private final EntityManager em;
public Optional<Reply> findById(int id) {
Reply reply = em.find(Reply.class, id);
return Optional.ofNullable(reply);
}
public Reply save(Reply reply) {
em.persist(reply);
return reply;
}
public void delete(Reply reply) {
em.remove(reply);
}
}4) detail.mustache
{{> header}}
<div class="container p-5">
{{#model.isOwner}}
<!-- 수정삭제버튼 -->
<div class="d-flex justify-content-end">
<a href = "/boards/{{model.id}}/update-form" class="btn btn-secondary me-1">수정</a>
<form action = "/boards/{{model.id}}/delete" method="post">
<button class="btn btn-outline-secondary">삭제</button>
</form>
</div>
{{/model.isOwner}}
<!-- 게시글내용 -->
<div>
<h2><b>{{model.title}}</b></h2>
<hr />
<div class="d-flex justify-content-end">
작성자 : {{model.username}}
</div>
<div class="m-4 p-2">
{{model.content}}
</div>
</div>
<!-- 댓글 -->
<div class="card mt-3">
<!-- 댓글등록 -->
<div class="card-body">
<form action="/replies/save" method="post">
<input type="hidden" name="boardId" value="{{model.id}}">
<textarea id="comment" class="form-control" rows="2" name="comment"></textarea>
<div class="d-flex justify-content-end">
<button class="btn btn-secondary mt-1">
댓글등록
</button>
</div>
</form>
</div>
<!-- 댓글목록 -->
<div class="card-footer">
<b>댓글리스트</b>
</div>
<div class="list-group">
{{#model.replies}}
<!-- 댓글아이템 -->
<div class="list-group-item d-flex justify-content-between align-items-center">
<div class="d-flex">
<div class="px-1 me-1 bg-secondary text-white rounded">{{replyUsername}}</div>
<div>{{comment}}</div>
</div>
{{#isReplyOwner}}
<form action="/replies/{{id}}/delete?boardId={{model.id}}" method="post">
<input type="hidden" name="boardId" value="{{model.id}}">
<button class="btn">🗑</button>
</form>
{{/isReplyOwner}}
</div>
{{/model.replies}}
</div>
</div>
</div>
</body>
</html>※ 로그인 안 한 상태에서 댓글 입력창 클릭했을 때
1) detail.mustache - 수정
① 코드
{{> header}}
<div class="container p-5">
{{#model.isOwner}}
<!-- 수정삭제버튼 -->
<div class="d-flex justify-content-end">
<a href = "/boards/{{model.id}}/update-form" class="btn btn-secondary me-1">수정</a>
<form action = "/boards/{{model.id}}/delete" method="post">
<button class="btn btn-outline-secondary">삭제</button>
</form>
</div>
{{/model.isOwner}}
<!-- 게시글내용 -->
<div>
<h2><b>{{model.title}}</b></h2>
<hr />
<div class="d-flex justify-content-end">
작성자 : {{model.username}}
</div>
<div class="m-4 p-2">
{{model.content}}
</div>
</div>
<!-- 댓글 -->
<div class="card mt-3">
<!-- 댓글등록 -->
<div class="card-body">
<form action="/replies/save" method="post">
<input type="hidden" name="boardId" value="{{model.id}}">
<textarea id="comment" class="form-control" rows="2" name="comment"></textarea>
<div class="d-flex justify-content-end">
<button class="btn btn-secondary mt-1">
댓글등록
</button>
</div>
</form>
</div>
<!-- 댓글목록 -->
<div class="card-footer">
<b>댓글리스트</b>
</div>
<div class="list-group">
{{#model.replies}}
<!-- 댓글아이템 -->
<div class="list-group-item d-flex justify-content-between align-items-center">
<div class="d-flex">
<div class="px-1 me-1 bg-secondary text-white rounded">{{replyUsername}}</div>
<div>{{comment}}</div>
</div>
{{#isReplyOwner}}
<form action="/replies/{{id}}/delete?boardId={{model.id}}" method="post">
<input type="hidden" name="boardId" value="{{model.id}}">
<button class="btn">🗑</button>
</form>
{{/isReplyOwner}}
</div>
{{/model.replies}}
</div>
</div>
</div>
{{^sessionUser}}
<script>
// 1. 조건 (querySelector 사용, textArea에 comment라는 id 하나 추가하기)
const textArea = document.querySelector("#comment");
// 2. textArea에 마우스로 클릭했을 때의 이벤트 리스너가 필요함
textArea.addEventListener("click", function () {
// 3. 이 때, 행위가 하나 필요함 -> 알림창을 띄워서 '로그인하세요'라고 알려줘야 함
alert("로그인하세요");
});
</script>
{{/sessionUser}}
</body>
</html>② 테스트

Share article