17 스프링부트(블로그만들기) 댓글추가 및 설명 V3 3.6

윤주헌's avatar
Sep 23, 2024
17 스프링부트(블로그만들기) 댓글추가 및 설명 V3 3.6
notion image
이러면 서버가 편안해진다
 
큰 회사 직접 CDN을 만든다!
 
2.0은 html이 서버에 요청하는데 분석을 서버에서 하고 받아야할 데이터 cs js등이 있는데 길 열어두고 한번 주고 한번 더 주는게 아닌 한번에 준다 물길 열어두고 리스펀스 길 열어두고 안 끊고 계속 준다!
 

면접 질문

만약 리로드 적혀있으면 땡 기본이 안되어있네 왜 ajax쓰는데 리로드 하냐? 물어봄
ajax 비동기 통신이요 라고 말하면 안된다 아니 정확하게요
비동기 통신해서 데이터만 받고 부분 리로딩하는 것
앗 실수했네요 하고 설명해야한다!
 
 
  • 알아야 할 것
💡
폼 태그 action 때리면 응답을 반드시 이 페이지 f5가 된다!! 중요
 
서버가 데이터 받는다 하면
 
리스트
<form action="/text/form" method="post"> <input type="text"name="username"> <button>고고</button> </form>
 
컨트롤러
@PostMapping("/test/form") public String form(){ return "user/join-form"; }
 
보이드면 어떻게 될까?
@PostMapping("/test/form") public void form(){ }
테스트에 테스트에? 찾는다?
결과
notion image
 
이번에는
@PostMapping("/test/form") public @ResponseBody String form(){ return "ok"; }
이러면 화면 ok됨
notion image
만약
@PostMapping("/test/form") public @ResponseBody String form(){ return """ //아무 html코드를 넣는다면 어떻게 될까? """; }
읽어지겠지?
 
잘 생각해보자 탬플릿 엔진 쓰는 이유는? 여기 return해도되잖아?
왜 안하냐?
편해서
 
탬플릿 앤진 역할
 
여기에 html 자바 코드 적어 그러면 컨트롤러에서 보내줄께
서버가 응답해주는데 파일로 보내는게 더 편해서
 
DB요청하고 값 받기
BoardResponse.DetailDTO board = boardService.게시글상세보기(User.builder().id(1).build(), 1);
이렇게 적을 수 잇는데
타이틀에 넣으려면 “”” “”” 이렇게 적으면 불편하다
 
“””.replace(”{{}}”)
내가 직접 바인딩 하는 것 보다 탬플릿 앤진에서 하는게 더 편해서!
 
폼테그 요청하면 응답하는데
js에 응답이 아닌 브라우저에 응답되고 반드시 페이지 리로드된다!!
 

중요

폼테그로 댓글 하면 부분리로딩이 안된다! 페이지 리로딩이 돼서 ajax쓰는 이유가 없다!

detail 머스테치 변경 form태그에서 ajax로

notion image
name, submit변경
<div class="card mt-3"> <!-- 댓글등록 --> <div class="card-body"> <form > <textarea class="form-control" rows="2" id="comment"></textarea> <div class="d-flex justify-content-end"> <button type="button" class="btn btn-outline-primary mt-1">댓글등록</button> </div> </form> </div>
 
id필요해? DB에 있잖아
user필요해? session값 들고오면 되니까
전송하게 되면 해킹할 수 있다
만약 1번 유저인데 이거를 클라이언트가 적어? 나는 10번이라고 말 할 수 있다
Board 꼭 필요
comment, Board id 꼭 필요!
 
<input type="hidden" value="{{sessionUser.id}} id="userId">
f12해보면 댓글 등록하는 곳에
userid가 1이라고 적힌다
이거는 누구나 변경할 수 있다 그래서 쓰면 안된다!! 반드시 히든X 세션으로 들거와야한다
 
이거는 괜찮음
<input type="hidden" value="{{sessionUser.id}}" id="boardId">
notion image
<form > <input type="hidden" value="{{sessionUser.id}}" id="boardId"> <div data-boardId="{{model.id}}"></div> <textarea class="form-control" rows="2" id="comment"></textarea> <div class="d-flex justify-content-end"> <button type="button" class="btn btn-outline-primary mt-1">댓글등록</button> </div> </form>
이러면

하려고 하는 것

하려고 하는 것
 
notion image
notion image
notion image
notion image
boardid boardid 중복돼서 안됐음
 
 

다시 데이터를 집어넣는거

notion image
<input type="hidden" value="{{sessionUser.id}}" id="boardId"> <div id="boardId2" data-hello="{{model.id}}"></div>
notion image
let boardId = $("#boardId2").data("hello"); alert(boardId); let boardId2 = document.querySelector("#boardId2").dataset.hello; console.log(boardId2);
결과
notion image
notion image
{{>layout/header}} <div class="container p-5"> <!-- 수정삭제버튼 --> {{#model.isOwner}} <div class="d-flex justify-content-end"> <a href="/api/board/{{model.id}}/update-form" class="btn btn-warning me-1">수정</a> <form action="/api/board/{{model.id}}/delete" method="post"> <button class="btn btn-danger">삭제</button> </form> </div> {{/model.isOwner}} <div class="d-flex justify-content-end"> <b>작성자</b> : {{model.username}} </div> <!-- 게시글내용 --> <div> <h2><b>{{model.title}}</b></h2> <hr/> <div class="m-4 p-2"> {{{model.content}}} </div> </div> <!-- 댓글 --> <div class="card mt-3"> <!-- 댓글등록 --> <div class="card-body"> <form > <input type="hidden" value="{{sessionUser.id}}" id="boardId"> <div id="boardId2" data-hello="{{model.id}}"></div> <textarea class="form-control" rows="2" id="comment"></textarea> <div class="d-flex justify-content-end"> <button type="button" class="btn btn-outline-primary mt-1">댓글등록</button> </div> </form> </div> <!-- 댓글목록 --> <div class="card-footer"> <b>댓글리스트</b> </div> <div class="list-group"> {{#model.replies}} <!-- 댓글아이템 --> <div id="reply-{{id}}" class="list-group-item d-flex justify-content-between align-items-center"> <div class="d-flex"> <div class="px-1 me-1 bg-primary text-white rounded">{{username}}</div> <div>{{comment}}</div> </div> {{#isOwner}} <form> <button onclick="deleteReply('{{id}}')" type="button" class="btn">🗑</button> </form> {{/isOwner}} </div> {{/model.replies}} </div> </div> </div> <script> let boardId = $("#boardId2").data("hello"); alert(boardId); let boardId2 = document.querySelector("#boardId2").dataset.hello; console.log(boardId2); async function deleteReply(id) { //1. header + body let response = await fetch(`/api/reply/${id}`, { method: "delete" }); // console.log(response); //2. body(parsing) let responseBody = await response.json();//파싱 // console.log(responseBody); if (response.ok) { $(`#reply-${id}`).remove(); //location.reload(); f5 때리는거 /* f5때리면 fetch 안보임 새로고침 전의 것이기에 이럴꺼면 form쓰지 즉 다시 다 다운받는 거임 다시 다 다운받는게 아닌 캐싱하는게 맞다! css js 이런것 정적대이터는 변하지 않는다. 내가 캐싱하고있는건가? 공부해야한다! 나중에 이런 데이터들은 어떻게 사용하냐면 스프링 서버 */ } else { alert(responseBody.msg); } } </script> {{>layout/footer}}

시작

디테일 변경

{{>layout/header}} <div class="container p-5"> <!-- 수정삭제버튼 --> {{#model.isOwner}} <div class="d-flex justify-content-end"> <a href="/api/board/{{model.id}}/update-form" class="btn btn-warning me-1">수정</a> <form action="/api/board/{{model.id}}/delete" method="post"> <button class="btn btn-danger">삭제</button> </form> </div> {{/model.isOwner}} <div class="d-flex justify-content-end"> <b>작성자</b> : {{model.username}} </div> <!-- 게시글내용 --> <div> <h2><b>{{model.title}}</b></h2> <hr/> <div class="m-4 p-2"> {{{model.content}}} </div> </div> <!-- 댓글 --> <div class="card mt-3"> <!-- 댓글등록 --> <div class="card-body"> <form > <input type="hidden" value="{{model.id}}" id="boardId"> <textarea class="form-control" rows="2" id="comment"></textarea> <div class="d-flex justify-content-end"> <button type="button" class="btn btn-outline-primary mt-1">댓글등록</button> </div> </form> </div> <!-- 댓글목록 --> <div class="card-footer"> <b>댓글리스트</b> </div> <div class="list-group"> {{#model.replies}} <!-- 댓글아이템 --> <div id="reply-{{id}}" class="list-group-item d-flex justify-content-between align-items-center"> <div class="d-flex"> <div class="px-1 me-1 bg-primary text-white rounded">{{username}}</div> <div>{{comment}}</div> </div> {{#isOwner}} <form> <button onclick="deleteReply('{{id}}')" type="button" class="btn">🗑</button> </form> {{/isOwner}} </div> {{/model.replies}} </div> </div> </div> <script> async function deleteReply(id) { //1. header + body let response = await fetch(`/api/reply/${id}`, { method: "delete" }); // console.log(response); //2. body(parsing) let responseBody = await response.json();//파싱 // console.log(responseBody); if (response.ok) { $(`#reply-${id}`).remove(); //location.reload(); f5 때리는거 /* f5때리면 fetch 안보임 새로고침 전의 것이기에 이럴꺼면 form쓰지 즉 다시 다 다운받는 거임 다시 다 다운받는게 아닌 캐싱하는게 맞다! css js 이런것 정적대이터는 변하지 않는다. 내가 캐싱하고있는건가? 공부해야한다! 나중에 이런 데이터들은 어떻게 사용하냐면 스프링 서버 */ } else { alert(responseBody.msg); } } </script> {{>layout/footer}}
form태그에 값 넣어 보내고 싶으면 hidden사용한다! 눈에 보일 필요는 없는데 가지고 가야 하잖아

시작 댓글 쓰기

api만들어야 해서 리플라이컨트롤러 이동

꼭 boardid와 comment는 꼭 필요하다
@PostMapping("/api/reply") public ResponseEntity<?> save(){ }

RequsetDTO만들어야 함

json방식은 save(String name) 이런거 값 못받음
package org.example.springv3.reply; import lombok.Data; public class ReplyRequest { @Data public static class SaveDTO{ private Integer boardId; private String comment; //toEntity만들어라!! 순수한 비영속 객체 만들어야 한다 ! insert할 때는 투 엔티티 public Reply toEntity(){ //리플리에 빌더 없음 } } }

빌더 없어서 Reply클래스에 빌더 만들어주기

package org.example.springv3.reply; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import jakarta.persistence.*; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import org.example.springv3.board.Board; import org.example.springv3.user.User; import org.hibernate.annotations.CreationTimestamp; import java.sql.Timestamp; @Table(name = "reply_tb") @Data @NoArgsConstructor @Entity public class Reply { @GeneratedValue(strategy = GenerationType.IDENTITY) @Id private Integer id; @ManyToOne(fetch = FetchType.LAZY) private User user; @ManyToOne(fetch = FetchType.LAZY) private Board board; private String comment; // 댓글 내용 @CreationTimestamp//em.persist 할 때 발동 private Timestamp createdAt; @Builder public Reply(Integer id, User user, Board board, String comment, Timestamp createdAt) { this.id = id; this.user = user; this.board = board; this.comment = comment; this.createdAt = createdAt; } }

다시 돌아가서

package org.example.springv3.reply; import lombok.Data; import org.example.springv3.board.Board; import org.example.springv3.user.User; public class ReplyRequest { @Data public static class SaveDTO{ private Integer boardId; private String comment; //원래는 /* insert into reply_tb(comment, board_id, user_id, created_at) values('댓글1', 5, 1, now()); 네이티브 쿼리 쓰면 insert끝나는데 jpa쓰려면 객체 만들어서 어자피 조회해야하지 않나? 그래서 board_id찾고 없으면 404 있으면 넣고 jpa는 객체를 퍼시스트한다! */ //toEntity만들어라!! 순수한 비영속 객체 만들어야 한다 ! insert할 때는 투 엔티티 public Reply toEntity(User sessionUser, Board board){ //리플리에 빌더 없음 return Reply.builder() .comment(comment) .user(null) .board(null) .build(); //보드 객체를 넣어야함 } } }

다시 리플리 컨트롤러

//ajax쓰고 있어서 save, delete 이런거 필요 없음 구분할거니까 //2개 받아야함!! 즉 reqeustDTO만들어야함 @PostMapping("/api/reply") public ResponseEntity<?> save(@RequestBody ReplyRequest.SaveDTO saveDTO){ User sessionUser = (User) session.getAttribute("sessionUser"); replyService.댓글쓰기(saveDTO, sessionUser); return ResponseEntity.ok(Resp.ok(null)); }
화면이 컨트롤러 요청 데이터 서비스한테 요청
세션은 save할 때 필요함!
alt enter하면 자동 생성
notion image

댓글 서비스 이동

public void 댓글쓰기(ReplyRequest.SaveDTO saveDTO, User sessionUser) { //1. 게시글 존재 유무 확인 //2. 댓글 저장 replyRepository.save(null); }
프라이머리키를 못 걸면 삭제를 못한다!
public Reply 댓글쓰기(ReplyRequest.SaveDTO saveDTO, User sessionUser) { //1. 게시글 존재 유무 확인 Board boardPs = boardRepository.findById(saveDTO.getBoardId()) .orElseThrow(() -> new ExceptionApi404("게시글을 찾을 수 없습니다.")); //2. 비영속 댓글 객체 만들기 Reply reply = saveDTO.toEntity(sessionUser, boardPs); //여기까지는 비영속 //3. 댓글 저장 Reply replyPS= replyRepository.save(reply); //담궈지자 마자 insert 프라이머리키 생긴다 return replyPS; }
replyPs도 되고
public Reply 댓글쓰기(ReplyRequest.SaveDTO saveDTO, User sessionUser) { //1. 게시글 존재 유무 확인 Board boardPs = boardRepository.findById(saveDTO.getBoardId()) .orElseThrow(() -> new ExceptionApi404("게시글을 찾을 수 없습니다.")); //2. 비영속 댓글 객체 만들기 Reply reply = saveDTO.toEntity(sessionUser, boardPs); //여기까지는 비영속 //3. 댓글 저장 replyRepository.save(reply); //담궈지자 마자 insert 프라이머리키 생긴다 return reply; }
reply 가능
 
비영속 객체가 ps 에 들어가서 바로 들어가니 바로 insert 되고 3번부터 reply가 된다!!
package org.example.springv3.reply; import lombok.RequiredArgsConstructor; import org.example.springv3.board.Board; import org.example.springv3.board.BoardRepository; import org.example.springv3.core.error.ex.ExceptionApi403; import org.example.springv3.core.error.ex.ExceptionApi404; import org.example.springv3.user.User; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Transactional(readOnly = true) @RequiredArgsConstructor @Service //컴포넌트 스켄 IOC에 넣기 public class ReplyService { private final ReplyRepository replyRepository; private final BoardRepository boardRepository; @Transactional public void 댓글삭제(int id, User sessionUser){ //애는 integer인지 어떻게 알았을까? 프라이머리키에 integer로 적어놔서 안다! //jpa레파지토리 만들기 어렵지 않다 다음에 만들어보자! 궁금하다고 하면 알려주심 제내릭만 잘 알면 된다! //삭제 전에 조회 먼저 하자 //ajax요청이니까 apiException터트려야 한다!! 받아줘야 하니까 앞에 reply에 PS 넣어줌 Reply replyPS = replyRepository.findById(id).orElseThrow( () -> new ExceptionApi404("해당 댓글을 찾을 수 없습니다") ); //지금 댓글만 조회할건데 User Board 안 내용은 비어있음 하지만 getUser.getId 포린키가 프라이머리키 들고있으니까 조회안함!! // BoardId, User Id 는 이미 있음 래이지 로딩은 안 내용은 없지만 프라이머리키는 가지고 있다 //즉 select한번 더 할 필요 없다 // 하지만 getusername()하려면 select한번 더 해야함 //이렇게 하려면 mFindById만들어서 join하는 쿼리 만들고 1번만 select할 수 있게 만들면 된다!! //findById하면 reply객체 만들어줌 id에 들어가고 comment들어가고 user, board에는 set id만 들어가 있고 안차있다!! if(replyPS.getUser().getId() != sessionUser.getId()){ throw new ExceptionApi403("댓글 삭제 권한이 없습니다"); //이런 것 전부가 트랜잭션이다 그래서 @Transaction붙여야 한다 } replyRepository.deleteById(id); } @Transactional public Reply 댓글쓰기(ReplyRequest.SaveDTO saveDTO, User sessionUser) { //1. 게시글 존재 유무 확인 Board boardPs = boardRepository.findById(saveDTO.getBoardId()) .orElseThrow(() -> new ExceptionApi404("게시글을 찾을 수 없습니다.")); //2. 비영속 댓글 객체 만들기 Reply reply = saveDTO.toEntity(sessionUser, boardPs); //여기까지는 비영속 //3. 댓글 저장(reply가 영속화됨) replyRepository.save(reply); //담궈지자 마자 insert 프라이머리키 생긴다 return reply; } }

댓글 response 클래스만들기

csr로 쓰래기통 보이고 안보이고 할 지 생각해야함
내가 적은 글은 바로 내 거니까 무조건 true
package org.example.springv3.reply; import lombok.Data; public class ReplyResponse { @Data public static class DTO{ private Integer id; private String comment; private String username; //깊은 복사 시작 public DTO(Reply reply) { this.id = reply.getId(); this.comment = reply.getComment(); this.username = reply.getUser().getUsername(); } } }
리플라이 객체 안에 user객체가 없어 board객체도 없이 비워져 있으면
lazy로딩 걸림 서비스 딴에서 getter해서 가지고온다??? 이거 모르겠음…

댓글 서비스

package org.example.springv3.reply; import lombok.RequiredArgsConstructor; import org.example.springv3.board.Board; import org.example.springv3.board.BoardRepository; import org.example.springv3.core.error.ex.ExceptionApi403; import org.example.springv3.core.error.ex.ExceptionApi404; import org.example.springv3.user.User; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Transactional(readOnly = true) @RequiredArgsConstructor @Service //컴포넌트 스켄 IOC에 넣기 public class ReplyService { private final ReplyRepository replyRepository; private final BoardRepository boardRepository; @Transactional public void 댓글삭제(int id, User sessionUser){ Reply replyPS = replyRepository.findById(id).orElseThrow( () -> new ExceptionApi404("해당 댓글을 찾을 수 없습니다") ); if(replyPS.getUser().getId() != sessionUser.getId()){ throw new ExceptionApi403("댓글 삭제 권한이 없습니다"); } replyRepository.deleteById(id); } @Transactional public ReplyResponse.DTO 댓글쓰기(ReplyRequest.SaveDTO saveDTO, User sessionUser) { //1. 게시글 존재 유무 확인 Board boardPs = boardRepository.findById(saveDTO.getBoardId()) .orElseThrow(() -> new ExceptionApi404("게시글을 찾을 수 없습니다.")); //2. 비영속 댓글 객체 만들기 Reply reply = saveDTO.toEntity(sessionUser, boardPs); //여기까지는 비영속 //3. 댓글 저장 replyRepository.save(reply); //담궈지자 마자 insert 프라이머리키 생긴다 return new ReplyResponse.DTO(reply); } }

리플리 컨트롤러 이동

@PostMapping("/api/reply") public ResponseEntity<?> save(@RequestBody ReplyRequest.SaveDTO saveDTO){ User sessionUser = (User) session.getAttribute("sessionUser"); ReplyResponse.DTO replyDTO = replyService.댓글쓰기(saveDTO, sessionUser); return ResponseEntity.ok(Resp.ok(replyDTO)); }

리뷰

ajax요청 api/reply 통해서
json데이터 받기 위해서는 DTO 받아서 @RequestBody를 붙여야 한다!
생각 해야 하는 것은 어떤 데이터를 클라이언트에게 받아야 하는가? (comment, boardId)
 
인증체크해야한다 (우리가 할 필요 없는게 API) 붙이면 되니까
 
유저 정보 받아와야 한다
insert할때 유저 정보가 필요해서 session에 받아서 댓글쓰기로 던진다!
 
서비스에서는 댓글 insert하고 프라이머리키 있는 애 가지고 오는 것
@Transactional public ReplyResponse.DTO 댓글쓰기(ReplyRequest.SaveDTO saveDTO, User sessionUser) { //1. 게시글 존재 유무 확인 Board boardPs = boardRepository.findById(saveDTO.getBoardId()) .orElseThrow(() -> new ExceptionApi404("게시글을 찾을 수 없습니다.")); //2. 비영속 댓글 객체 만들기 Reply reply = saveDTO.toEntity(sessionUser, boardPs); //여기까지는 비영속 //3. 댓글 저장 replyRepository.save(reply); //담궈지자 마자 insert 프라이머리키 생긴다 return new ReplyResponse.DTO(reply); }
 
만약 jpa로 안 쓰면
toEntity 필요 없음 서비스에서 게시글 존재유무, 비영속 이런거 다 필요 없고 레파지토리에서 @Repository public class ReplQueryRespository{ EntityManager em; public void insert(ResplyRequest.SaveDTO saveDTO,, insert userId){ 예전 쿼리 만들기 } }
 
댓글쓰기할 때
private final ReplyQueryRepository repository; replyQueryRepository.insert(saveDTO
빡시다 다시 select해야해서!
 
insert실행된다 해도 프라이머리키를 알 수 없다 그래서
 
public int maxId(){ Query query em.createNativeQuery("select max(id) from reply_tb", Long.class); Long replyId = (Long)query.getSingleResult(); return replyId.intValue(); }
서비스에서 insert했고
밑에
int replyId = replyqueryRepository.maxId(); //리턴해줘야 한다! 예 트랜잭션 걸려있다 딴 놈은 못함 maxId하면 내가 isnert한 데이터 맞음 그럼 다시 db가서 조회해야 한다
또 finbyId 필요하다
 
Reply reply = replyQueryRepository.findById(replyId);
return new ReplyResponse.DTO(reply);
 
알아서 동기화 시키니까 내가 maxId 이런것 다 필요 없이 알아서 찾아주니까 좋다!
 
 
  1. 게시글 존재 유무확인
  1. 인서트하기
  1. MaxId 찾기
  1. MaxId로 댓글 조회하기
  1. DTO 응답
 

패치로 보내보자!

 

디테일 머스테치

{{>layout/header}} <div class="container p-5"> <!-- 수정삭제버튼 --> {{#model.isOwner}} <div class="d-flex justify-content-end"> <a href="/api/board/{{model.id}}/update-form" class="btn btn-warning me-1">수정</a> <form action="/api/board/{{model.id}}/delete" method="post"> <button class="btn btn-danger">삭제</button> </form> </div> {{/model.isOwner}} <div class="d-flex justify-content-end"> <b>작성자</b> : {{model.username}} </div> <!-- 게시글내용 --> <div> <h2><b>{{model.title}}</b></h2> <hr/> <div class="m-4 p-2"> {{{model.content}}} </div> </div> <!-- 댓글 --> <div class="card mt-3"> <!-- 댓글등록 --> <div class="card-body"> <form> <input type="hidden" value="{{model.id}}" id="boardId"> <textarea class="form-control" rows="2" id="comment"></textarea> <div class="d-flex justify-content-end"> <button onclick="saveReply()" type="button" class="btn btn-outline-primary mt-1">댓글등록</button> </div> </form> </div> <!-- 댓글목록 --> <div class="card-footer"> <b>댓글리스트</b> </div> <div class="list-group"> {{#model.replies}} <!-- 댓글아이템 --> <div id="reply-{{id}}" class="list-group-item d-flex justify-content-between align-items-center"> <div class="d-flex"> <div class="px-1 me-1 bg-primary text-white rounded">{{username}}</div> <div>{{comment}}</div> </div> {{#isOwner}} <form> <button onclick="deleteReply('{{id}}')" type="button" class="btn">🗑</button> </form> {{/isOwner}} </div> {{/model.replies}} </div> </div> </div> <script> async function saveReply() { //1. Reply 객체 만들기(ID로 찾아서) //이거 그대로 전송 못해서 json으로 변경해서 보내야함 let reply = { comment: $("#comment").val(), boardId: $("#boardId").val() }; //2. fetch 요청하기 메서드, 어떤 마임타입가지고 있는지(컨탠트 타입) get은 where절에 걸려고 쓰니까 쿼리 스트링이다 let response = await fetch(`/api/reply`, { method: "post", body: JSON.stringify(reply), headers: { "Content-Type": "application/json; charset=utf-8" } }); //파싱 let responseBody = await response.json(); // DTO!! console.log(responseBody); //3.CSR 하기 } async function deleteReply(id) { //1. header + body let response = await fetch(`/api/reply/${id}`, { method: "delete" }); // console.log(response); //2. body(parsing) let responseBody = await response.json();//파싱 // console.log(responseBody); if (response.ok) { $(`#reply-${id}`).remove(); //location.reload(); f5 때리는거 /* f5때리면 fetch 안보임 새로고침 전의 것이기에 이럴꺼면 form쓰지 즉 다시 다 다운받는 거임 다시 다 다운받는게 아닌 캐싱하는게 맞다! css js 이런것 정적대이터는 변하지 않는다. 내가 캐싱하고있는건가? 공부해야한다! 나중에 이런 데이터들은 어떻게 사용하냐면 스프링 서버 */ } else { alert(responseBody.msg); } } </script> {{>layout/footer}}
notion image
오류난게
package org.example.springv3.reply; import lombok.Data; import org.example.springv3.board.Board; import org.example.springv3.user.User; public class ReplyRequest { @Data public static class SaveDTO{ private Integer boardId; private String comment; //원래는 /* insert into reply_tb(comment, board_id, user_id, created_at) values('댓글1', 5, 1, now()); 네이티브 쿼리 쓰면 insert끝나는데 jpa쓰려면 객체 만들어서 어자피 조회해야하지 않나? 그래서 board_id찾고 없으면 404 있으면 넣고 jpa는 객체를 퍼시스트한다! */ //toEntity만들어라!! 순수한 비영속 객체 만들어야 한다 ! insert할 때는 투 엔티티 public Reply toEntity(User sessionUser, Board board){ //리플리에 빌더 없음 return Reply.builder() .comment(comment) .user(sessionUser) .board(board) .build(); //보드 객체를 넣어야함 } } }

디버깅 먼저 뿌려보기! 시스아웃

 
@Slyfj
 
  1. 디버깅해도 모르면
sout해서 어디 부분에서 잘못됐는지 확인 추적하기!!
 
  1. save 아래 빨간점 찍고 벌래 실행 벌래로 실행
스텝오버 f8
다시 재 실행actuator 옆 버튼 누르기
스탭인투 F7 메서드 내부로 들어가라!
스텝아웃 시프트 F8 빠져나오기
 

마지막 부분

로케이션 리로드 //어팬드? 프리팬드??? 찾아보자

어팬드 해야한다!!
{{>layout/header}} <div class="container p-5"> <!-- 수정삭제버튼 --> {{#model.isOwner}} <div class="d-flex justify-content-end"> <a href="/api/board/{{model.id}}/update-form" class="btn btn-warning me-1">수정</a> <form action="/api/board/{{model.id}}/delete" method="post"> <button class="btn btn-danger">삭제</button> </form> </div> {{/model.isOwner}} <div class="d-flex justify-content-end"> <b>작성자</b> : {{model.username}} </div> <!-- 게시글내용 --> <div> <h2><b>{{model.title}}</b></h2> <hr/> <div class="m-4 p-2"> {{{model.content}}} </div> </div> <!-- 댓글 --> <div class="card mt-3"> <!-- 댓글등록 --> <div class="card-body"> <form> <input type="hidden" value="{{model.id}}" id="boardId"> <textarea class="form-control" rows="2" id="comment"></textarea> <div class="d-flex justify-content-end"> <button onclick="saveReply()" type="button" class="btn btn-outline-primary mt-1">댓글등록</button> </div> </form> </div> <!-- 댓글목록 --> <div class="card-footer"> <b>댓글리스트</b> </div> <div class="list-group"> {{#model.replies}} <!-- 댓글아이템 --> <div id="reply-{{id}}" class="list-group-item d-flex justify-content-between align-items-center"> <div class="d-flex"> <div class="px-1 me-1 bg-primary text-white rounded">{{username}}</div> <div>{{comment}}</div> </div> {{#isOwner}} <form> <button onclick="deleteReply('{{id}}')" type="button" class="btn">🗑</button> </form> {{/isOwner}} </div> {{/model.replies}} </div> </div> </div> <script> //리로드 안 하려고 function replyItem(reply) { return ` <div id="reply-${reply.id}" class="list-group-item d-flex justify-content-between align-items-center"> <div class="d-flex"> <div class="px-1 me-1 bg-primary text-white rounded">${reply.username}</div> <div>${reply.comment}</div> </div> <form> <button onclick="deleteReply('${reply.id}')" type="button" class="btn">🗑</button> </form> </div>`; } //어팬드? 프리팬드??? 찾아보자 async function saveReply() { //1. Reply 객체 만들기(ID로 찾아서) //이거 그대로 전송 못해서 json으로 변경해서 보내야함 let reply = { comment: $("#comment").val(), boardId: $("#boardId").val() }; //2. fetch 요청하기 메서드, 어떤 마임타입가지고 있는지(컨탠트 타입) get은 where절에 걸려고 쓰니까 쿼리 스트링이다 let response = await fetch(`/api/reply`, { method: "post", body: JSON.stringify(reply), headers: { "Content-Type": "application/json; charset=utf-8" } }); //파싱 let responseBody = await response.json(); // DTO!! console.log(responseBody); //3.CSR 하기 //이거는 전체 리로드 //location.reload(); } async function deleteReply(id) { //1. header + body let response = await fetch(`/api/reply/${id}`, { method: "delete" }); // console.log(response); //2. body(parsing) let responseBody = await response.json();//파싱 // console.log(responseBody); if (response.ok) { $(`#reply-${id}`).remove(); //location.reload(); f5 때리는거 /* f5때리면 fetch 안보임 새로고침 전의 것이기에 이럴꺼면 form쓰지 즉 다시 다 다운받는 거임 다시 다 다운받는게 아닌 캐싱하는게 맞다! css js 이런것 정적대이터는 변하지 않는다. 내가 캐싱하고있는건가? 공부해야한다! 나중에 이런 데이터들은 어떻게 사용하냐면 스프링 서버 */ } else { alert(responseBody.msg); } } </script> {{>layout/footer}}
박스를 찾아야 하는데 없음 그래서 만들어준다
 
최종
{{>layout/header}} <div class="container p-5"> <!-- 수정삭제버튼 --> {{#model.isOwner}} <div class="d-flex justify-content-end"> <a href="/api/board/{{model.id}}/update-form" class="btn btn-warning me-1">수정</a> <form action="/api/board/{{model.id}}/delete" method="post"> <button class="btn btn-danger">삭제</button> </form> </div> {{/model.isOwner}} <div class="d-flex justify-content-end"> <b>작성자</b> : {{model.username}} </div> <!-- 게시글내용 --> <div> <h2><b>{{model.title}}</b></h2> <hr/> <div class="m-4 p-2"> {{{model.content}}} </div> </div> <!-- 댓글 --> <div class="card mt-3"> <!-- 댓글등록 --> <div class="card-body"> <form> <input type="hidden" value="{{model.id}}" id="boardId"> <textarea class="form-control" rows="2" id="comment"></textarea> <div class="d-flex justify-content-end"> <button onclick="saveReply()" type="button" class="btn btn-outline-primary mt-1">댓글등록</button> </div> </form> </div> <!-- 댓글목록 --> <div class="card-footer"> <b>댓글리스트</b> </div> <div class="list-group" id="reply-box"> {{#model.replies}} <!-- 댓글아이템 --> <div id="reply-{{id}}" class="list-group-item d-flex justify-content-between align-items-center"> <div class="d-flex"> <div class="px-1 me-1 bg-primary text-white rounded">{{username}}</div> <div>{{comment}}</div> </div> {{#isOwner}} <form> <button onclick="deleteReply('{{id}}')" type="button" class="btn">🗑</button> </form> {{/isOwner}} </div> {{/model.replies}} </div> </div> </div> <script> //리로드 안 하려고 function replyItem(reply) { return ` <div id="reply-${reply.id}" class="list-group-item d-flex justify-content-between align-items-center"> <div class="d-flex"> <div class="px-1 me-1 bg-primary text-white rounded">${reply.username}</div> <div>${reply.comment}</div> </div> <form> <button onclick="deleteReply('${reply.id}')" type="button" class="btn">🗑</button> </form> </div>`; } //어팬드? 프리팬드??? 찾아보자 async function saveReply() { //1. Reply 객체 만들기(ID로 찾아서) //이거 그대로 전송 못해서 json으로 변경해서 보내야함 let reply = { comment: $("#comment").val(), boardId: $("#boardId").val() }; //2. fetch 요청하기 메서드, 어떤 마임타입가지고 있는지(컨탠트 타입) get은 where절에 걸려고 쓰니까 쿼리 스트링이다 let response = await fetch(`/api/reply`, { method: "post", body: JSON.stringify(reply), headers: { "Content-Type": "application/json; charset=utf-8" } }); //파싱 let responseBody = await response.json(); // DTO!! console.log(responseBody); //3.CSR 하기 //이거는 전체 리로드 //location.reload(); //왜 바디인가? 리스펀스 바디 . 바디에 들어가 있기 때문에!! $("#reply-box").prepend(replyItem(responseBody.body)); //공백 넣으려고 $("textarea").val(""); } async function deleteReply(id) { //1. header + body let response = await fetch(`/api/reply/${id}`, { method: "delete" }); // console.log(response); //2. body(parsing) let responseBody = await response.json();//파싱 // console.log(responseBody); if (response.ok) { $(`#reply-${id}`).remove(); //location.reload(); f5 때리는거 /* f5때리면 fetch 안보임 새로고침 전의 것이기에 이럴꺼면 form쓰지 즉 다시 다 다운받는 거임 다시 다 다운받는게 아닌 캐싱하는게 맞다! css js 이런것 정적대이터는 변하지 않는다. 내가 캐싱하고있는건가? 공부해야한다! 나중에 이런 데이터들은 어떻게 사용하냐면 스프링 서버 */ } else { alert(responseBody.msg); } } </script> {{>layout/footer}}
 
Share article

code-sudal