스프링/스프링부트+JPA - 블로그

스프링부트+JPA 블로그 프로젝트 11 댓글

H-V 2021. 9. 2. 21:24

유투버 '데어프로그래밍'님 강의 참조

 

▲ 댓글/댓글 리스트 디자인

  • detail.js -> w3schools 의 card를 참고 했음
<div class="card">
<div class="card-header">댓글 리스트</div>
<ul id="reply-box" class="list-group">
  <li id="reply-1"class="list-group-item d-flex justify-content-between">
  <div>댓글 내용</div>
  <div class="d-flex">
  <div class="font-italic">작성자:asdadasds &nbsp;</div>
  <button class="badge">삭제</button>
  </div>
  </li>
</ul>
</div>
</div>

 

 

▲ 댓글 목록 뿌리기 

  • ReplyRepository
public interface ReplyRepository extends JpaRepository<Reply, Integer>{}
  • 이미 Board 모델에 reply를 mappedby로 들고오기때문에 jsp에서 찾아서 뿌리기만 하면 된다.
    하지만 지금 무한참조 오류가 있다. 무한참조 오류가 나는 이유는 reply 모델 객체가 board 객체를 받음으로써 board객체를 부를때 board객체 안의 모든 변수 + reply/user까지 잘불러지고 user또한 잘 불러지는데 reply에 와서 reply가 가지고있는 board객체를 보고 다시 board를 부르게 되기 때문, 즉 reply모델이 호출될때는 board가 안불러지게 세팅하면 된다

  • 무한참조 간단 해결법은 @JsonIgnoreProperties. Board에서 reply를 호출하면 이 어노테이션이 걸린부분을 걸러준다. 직접 Reply를 호출하면 걸러지지 않는다. 
	@ManyToOne
	@JsonIgnoreProperties({"board"})
	@JoinColumn(name="boardId")
	private Board board;

굿!

 

 

▲댓글 작성

  • 댓글 순서를 바꾸는 어노테이션 @OrderBy
public class Board {
	..
	@OneToMany(mappedBy="board", fetch = FetchType.EAGER)
	@JsonIgnoreProperties({"board"})
	@OrderBy("id desc")
	private List<Reply> replies;
	...
}
  • detail.jsp 에 아이디를 받을 코드 추가
<form>
<input type="hidden" id="boardId" value="${board.id}"/>
<div>
<div class="card-body">
<textarea id="reply-content" class="form-control" row="1" cols="3"></textarea>
</div>
<div class="card-footer">
<button type="button" id="reply-save"class="btn btn-primary">댓글 쓰기</button>
</div>
</div>
</form>
  • board.js 에 아이디를 받아서 뿌리기
let index = {
	init: function() {
    	...
        	$("#btn-reply-save").on("click", () => { 
			this.replySave();
		});
	},
    ..
   replySave: function() {
		let data = {
			content: $("#reply-content").val()
		};
		console.log(data)
		
		let boardId = $("#boardId").val();
		$.ajax({
			type: "post",
			url: `/api/board/${boardId}/reply`,
			data: JSON.stringify(data),//HTTP BODY DATA
			contentType: "application/json; charset=utf-8", //body데이터의 타입
			dataType: "json" //서버에서 받을 데이터 형식, 즉 json으로 던지고 서버를위해 자동 파싱 = JSON->JS
		}).done(function(resp) { //위의 데이터가 js로 바뀌고 파라미터로 사용 가능
			//통신이 정상이면 done
			alert("댓글 추가 성공!");
			location.href = `/board/${boardId}`;
		}).fail(function(error) {
			//통신이 비정상이면 fail
			alert(JSON.stringify(error));
		});
	},
}
index.init();
  • BoardApiController
	//댓글 등록 요청
	@PostMapping("/api/board/{boardId}/reply")
	public ResponseDto<Integer> replySave(@PathVariable int boardId,
			@RequestBody Reply reply,
			@AuthenticationPrincipal PrincipalDetail principal) {
		boardService.reply(principal.getUser(),boardId,reply);
		return new ResponseDto<Integer>(HttpStatus.OK.value(),1);
	}
  • BoardService
	//댓글 쓰기
	@Transactional
	public void reply(User user, int boardId, Reply requestReply) {
		Board board = boardRepository.findById(boardId)
				.orElseThrow(()->{
					return new IllegalArgumentException("댓글 달기 실패!");
				}); //영속화
		requestReply.setUser(user);
		requestReply.setBoard(board);
		replyRepository.save(requestReply);
	}

굿 !

 

 

 

▲ 댓글을 위한 Dto 만들기

  • ReplySaveRequestDto
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ReplySaveRequestDto {
	private int userId;
	private int boardId;
	private String content;
}
  • BoardApiController
	//데이터 받을때 컨트롤러에서 Dto를 만들어 주는게 좋다
	//댓글 등록 요청
	@PostMapping("/api/board/{boardId}/reply")
	public ResponseDto<Integer> replySave(@RequestBody ReplySaveRequestDto replySaveRequestDto) {
		boardService.reply(replySaveRequestDto);
		return new ResponseDto<Integer>(HttpStatus.OK.value(),1);
	}
  • BoardService
	//댓글 쓰기
	@Transactional
	public void reply(ReplySaveRequestDto replySaveRequestDto) {
		Board board = boardRepository.findById(replySaveRequestDto.getBoardId())
				.orElseThrow(()->{
					return new IllegalArgumentException("댓글 달기 실패!->보드 ID 찾을 수 없음");
				}); //영속화
		User user = userRepository.findById(replySaveRequestDto.getUserId())
				.orElseThrow(()->{
					return new IllegalArgumentException("댓글 달기 실패!->유저 ID 찾을 수 없음");
				}); //영속화
//		Reply reply = new Reply();
//		reply.update(user, board, replySaveRequestDto.getContent());
		
		Reply reply = Reply.builder()
				.user(user)
				.board(board)
				.content(replySaveRequestDto.getContent())
				.build();
		replyRepository.save(reply);
	}
  • 시큐리티가 걸려있는 유저아이디도 detail.js에서 hidden으로 받고 board.js에 뿌리기
<input type="hidden" id="userId" value="${principal.user.id}"/>
	replySave: function() {
		let data = {
			userId:$("#userId").val(),
			boardId:$("#boardId").val(),
			content: $("#reply-content").val()
		};
		$.ajax({
			type: "post",
			url: `/api/board/${data.boardId}/reply`,
			data: JSON.stringify(data),//HTTP BODY DATA
			contentType: "application/json; charset=utf-8", //body데이터의 타입
			dataType: "json" //서버에서 받을 데이터 형식, 즉 json으로 던지고 서버를위해 자동 파싱 = JSON->JS
		}).done(function(resp) { //위의 데이터가 js로 바뀌고 파라미터로 사용 가능
			//통신이 정상이면 done
			alert("댓글 추가 성공!");
			location.href = `/board/${data.boardId}`;
		}).fail(function(error) {
			//통신이 비정상이면 fail
			alert(JSON.stringify(error));
		});
	},

굿!

 

  • 정리를하자면 Dto를 만들기전에는 일일이 받아야할 파라미터를 하나의 함수에 하나씩 하나씩 다 넣어 줬다. 그래서 컨트롤러, 서비스에 받는 파라미터가 많고 일일이 타이핑을 해야하는데 프로젝트가 커지면 실수할 확률이 높다. 그래서 DTO를 만들어서 한번에 받아오도록 관리하면 쉽다.
  • 더 나아가 builder보다는 아래처럼 만드는거 또한 더 좋은 방법이다
public class Reply {
....
	public void update(User user, Board board, String content) {
		setUser(user);
		setBoard(board);
		setContent(content);
	}
}


@Service
public class BoardService {
	...
    //댓글 쓰기
	@Transactional
	public void reply(ReplySaveRequestDto replySaveRequestDto) {
    ...
		Reply reply = new Reply();
		reply.update(user, board, replySaveRequestDto.getContent());
	...

 

 

☞ 하지만 지금도 오브젝트를 만들고 오브젝트를 빌더하고 영속화 시키는등 귀찮은 일이 많다. 네이티브 쿼리를 사용해서 조금 더 편하게 해보자

  • ReplyRepository에서 쿼리문을 작성하면 쉽다 (Dto 순서와 꼭 맞춰주자)
public interface ReplyRepository extends JpaRepository<Reply, Integer>{
	@Modifying
	@Query(value="INSERT INTO reply(userId, boardId, content, createdate) VALUES(?1,?2,?3,now())", nativeQuery = true)
	int mSave(int userId, int boardId, String content);
}
  • int로 리턴 타입을 맞추는 이유는 JDBC자체에서 업데이트시 값을 업데이트 행의 개수를 리턴해주기 때문이다. 
  • BoardService가 엄청 간단해 진다.
	//댓글 쓰기
	@Transactional
	public void reply(ReplySaveRequestDto replySaveRequestDto) {
		replyRepository.mSave(replySaveRequestDto.getUserId()
				,replySaveRequestDto.getBoardId()
				,replySaveRequestDto.getContent());
	}

굿!

※ 서버에 무언가를 찍어서 테스트를 하고 싶으면 toString() 메소드를 쓰면 좋다. (오브젝트를 호출하면 toString이 불러진다)

  • Reply 모델에 toString을 세팅
	@Override
	public String toString() {
		return "Reply [id=" + id + ", content=" + content + ", createDate=" + createDate + ", board=" + board
				+ ", user=" + user + "]";
	}
  • 테스용 클래스 생성

public class ReplyObjectTest {
	
	@Test
	public void toStringTest() {
		Reply reply = Reply.builder()
				.id(1)
				.user(null)
				.board(null)
				.content("Hi")
				.build();
		System.out.println(reply);
	}
}

굿!

 

 

 

▲ 간단 @Autowired 원리 짚고 넘어 가기

  • DI의 하나로써 스프링이 컴포넌트를 스캔해서 컨테이너 IoC에 빈을 등록하고 필요할때 스프링이 알아서 어노테이션을보고 주입을 시켜주는 원리
  • @Autowired는 생성자를 만드는것과 같다고 보면 되고 아래의 3개 방법은 동일한 기능을 한다
@Service
public class BoardService {
	
      @Autowired
      private BoardRepository boardRepository;

      @Autowired
      private ReplyRepository replyRepository;
}
    
    ==
    
public class BoardService(BoardRepository bRepo, ReplyRepository rRepo) {
      this.boardRepository = bRepo;
      this.replyRepository = rRepo;
}

	==

@Service
@RequriedArgsConstructor
public class BoardService {
      private final BoardRepository boardRepository;
      private final ReplyRepository replyRepository;
}

 

 

 

 

▲ 현재 2가지의 문제가 있다. 1)같은 아이디로 가입을 시도하면 가입 완료라고 뜨지만 서버에 에러가 뜬다. 해결하자!

  • 현재 예외처리 핸들러에서 모든 예외와 오류를 처리하고 오류시에는 500이라는 숫자를 띄운다.
@ControllerAdvice
@RestController
public class GlobalExceptionHandler {
	@ExceptionHandler(value = Exception.class)
	public ResponseDto<String> handleArgumentException(Exception e) {
		return new ResponseDto<String>(HttpStatus.INTERNAL_SERVER_ERROR.value(),e.getMessage());
	}
}

  • JS에서 회원가입 실패 코드만 하나 추가해주자!
		}).done(function(resp) { //위의 데이터가 js로 바뀌고 파라미터로 사용 가능
			//통신이 정상이면 done
			if (resp.status == 500) {
				alert("회원가입 실패!");
			} else {
				alert("회원가입 완료!");
				location.href = "/";

굿!

 

▲ 2) 댓글이 있는 글 삭제시 삭제 완료라고 하지만 글 삭제가 되지 않는다. -> 포린키가 걸려있고 게시글 삭제시 게시물과 댓글 다 삭제되도록 세팅을 해야 한다.

  • Board 모델에 reply 객체에 캐스캐이딩을 추가한다

*Cascade란? 

부모 엔티티가 영속화될때, 자식 엔티티도 같이 영속화되고 부모 엔티티가 삭제 될때, 자식 엔티티도 삭제되는 등 부모의 영속성 상태가 전이되는 것을 이야기한다

	@OneToMany(mappedBy="board", fetch = FetchType.EAGER, cascade = CascadeType.REMOVE)
	@JsonIgnoreProperties({"board"})
	@OrderBy("id desc")
	private List<Reply> replies;

굿!

 

 

 

 

 

♣ 댓글 삭제 하기

  • detail.jsp 코드 수정 및 삭제 버튼 이벤트 걸기 (아이디가 다르면 삭제버튼 안보이도록 !)
<div class="card">
<div class="card-header">댓글 리스트</div>
<ul id="reply-box" class="list-group">
<c:forEach var="reply" items="${board.replies}">
<li id="reply-${reply.id}" class="list-group-item d-flex justify-content-between">
<div>${reply.content}</div>
<div class="d-flex">
<div class="font-italic">작성자: ${reply.user.username} &nbsp;</div>
<c:if test="${reply.user.id==principal.user.id}">
<button onClick="index.replyDelete(${board.id}, ${reply.id})" class="badge">삭제</button>
</c:if>
</div>
</li>
</c:forEach>
</ul>
</div>
  • board.js의 delete 용 추가 (이벤트이기때문에 단순 ajax만 추가)
	replyDelete: function(boardId,replyId) {
		$.ajax({
			type: "delete",
			url: `/api/board/${boardId}/reply/${replyId}`,
			contentType: "application/json; charset=utf-8", //body데이터의 타입
			dataType: "json" //서버에서 받을 데이터 형식, 즉 json으로 던지고 서버를위해 자동 파싱 = JSON->JS
		}).done(function(resp) { //위의 데이터가 js로 바뀌고 파라미터로 사용 가능
			//통신이 정상이면 done
			alert("댓글 삭제 성공!");
			location.href = `/board/${boardId}`;
		}).fail(function(error) {
			//통신이 비정상이면 fail
			alert(JSON.stringify(error));
		});
	},
  • BoardApiController에 댓글 삭제 추가
	//댓글 삭제 요청
	@DeleteMapping("/api/board/{boardId}/reply/{replyId}")
	public ResponseDto<Integer> replyDelete(@PathVariable int replyId){
		boardService.deleteReply(replyId);
		return new ResponseDto<Integer>(HttpStatus.OK.value(),1);
	}
  • BoardService
	//댓글 삭제
	@Transactional
	public void deleteReply(int replyId) {
		replyRepository.deleteById(replyId);
	}

굿! 아이디가 다르면 삭제버튼이 보이지 않는다

 

 

blog.zip
0.21MB