스프링/스프링부트+JPA - 인스타
스프링부트+JPA - 인스타그램 클론 코딩 12 - 댓글(모델링/삽입)
H-V
2021. 10. 19. 13:34
이지업 최주호 강사님 강의 참조
01 댓글 모델링
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Data
@Entity
public class Comment {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
@Column(length = 100, nullable = false)
private String contentString;
@JoinColumn(name = "userId")
@ManyToOne(fetch = FetchType.EAGER)
private User user;
@JoinColumn(name = "imageId")
@ManyToOne(fetch = FetchType.EAGER)
private Image image;
private LocalDateTime createDate;
@PrePersist
public void createDate() {
this.createDate = LocalDateTime.now();
}
}
- 다른 모델들과 동일하다 필요한것을 선언하고 넣어주자. (길이와 널값처리는 개발자의 의도에 맞게 넣자)
- 댓글을 쓸때에는 그 댓글을 쓰는 'User' 정보와 그 댓글이 달리는 'Image'의 정보가 필요하다. 즉 모든 모델링에서 그 모델링이 필요한 다른 모델링이 어떤게 있을까 라고 생각하고 구현하는게 좋다
- 유저와 이미지 클래스가 필요하니 같이 선언해주고 다른 테이블이 들고있는 외래키를 @JoinColumn으로 묶어 준다
- 연관관계 매핑시에 항상 주 클래스와 땡겨서 넣는 클래스의 관계를 잘 생각하고 주 클래스가 앞에오도록 연관관계를 설정한다
- 'fetch' 전략에서는 EAGER/LAZY 전략으로 나뉘는데 한 클래스를 호출시에 딸려오는 다른 클래스가 단순히 하나라면 EAGER, 여러개라면 LAZY로 처리하는게 좋다
(예: 댓글같은경우는 한 이미지에 한 댓글이 적히는 형태이다(여러개를 쓸수는있지만 한번에 여러개를 동시에 못한다) 즉 한 호출에 하나만 딸려오면 EAGER.
(예: 프로필을 불러올때 프로필로 올린 여러 이미지는 한번에 여러개가 불러온다. 한 호출에 여러개가 불러지면 이때는 LAZY.)
01 - 1 레퍼지토리
- 레퍼지토리를 만드는것은 정말 일도 아니다. 제일 쉽다!
public interface CommentRepository extends JpaRepository<Comment, Integer>{}
02 AJAX 통신 방법 세팅
- 예전 스토리로드 전체를 구현했을때 AJAX로 'storyLoad()'라는 함수를 호출했고 이 함수가 'getStoryItem()'를 불러왔다. 이 getStoryItem() 함수가 'story.jsp'의 스토리 목록을 뿌리는 부분을 들고 있다.
- 숫자 1로 표현되어있던부분을 이제 image.id 로 바꿔주자
아이디 + 각각의 유저 사진들을 바꿔주면 F12에서 나오는 오류가 사라진다 - 댓글 작성시 onClick이 발동되면서 모델링에 세팅되어있든 값들이 필요한데 문제는 userId/imageId를 어디서 받아오냐 이다.
이미지 아이디값을 클릭을 하면 AJAX로 받도록 하고 userId 값은 세션으로 처리하면 된다! - addComment()함수에 imageId를 파라미터로 받고 이 함수가 작동되면 뿌릴 수 있도록 댓글 화면에도 아이디값들을 변경할 필요가 있다
storyCommentItem값은 나중에 댓글 삽입 후 꺼내오기만 하면 된다. 변경이 잘 되었다 - 이제 댓글을 삽입 시키고 새로운 댓글들이 append 되도록 만들면 된다
첫번째 AJAX 세팅
03 컨트롤러+서비스 구현
- AJAX 1차 세팅을 기반으로 컨트롤러 + 서비스를 구현해 보자
- 현재 AJAX에서 'data'로 넘어오는 값을 받아서 DB에 넣는 작업을 구현할텐데 현재 넘어오는 값은 imageId와 content인데 모델링에서 Comment 클래스 객체로 받을 수 없다. 즉 DTO로 만들어서 원하는 데이터만 받을 수 있도록 해야 한다
- DTO 세팅
@Data public class CommentDto { private String content; private int imageId; }
- CommentApiController
DTO를 이용해 원하는 값을 받고, 거기다 userId값을 또 받아야하니 세션이 이미 들어가있는 @AuthenticationPrincipal을 통해서 유저값을 끌어오면 된다. 그리고 리턴으로 응답값/메세지/데이터를 돌려주자@RequiredArgsConstructor @RestController public class CommentApiController { private final CommentService commentService; @PostMapping("/api/comment") public ResponseEntity<?> inserComment(@RequestBody CommentDto commentDto, @AuthenticationPrincipal PrincipalDetails principalDetails){ //System.out.println(commentDto); Comment comment = commentService.insertCommnet(commentDto.getContent(), commentDto.getImageId(), principalDetails.getUser().getId()); return new ResponseEntity<>(new CMRespDto<>(1, "Success", comment), HttpStatus.CREATED); }
- CommentService
서비스가 호출되면 DB에 접근 가능하도록 값들을 세팅해준다. 여기서 ID값을 받아올때 일일이 객체를 셀렉트해서 일일이 받아와야하지만 그렇게하지말고 '가짜 객체'라는것을 만들어서 ID값만 심플하게 받아올수있다.@RequiredArgsConstructor @Service public class CommentService { private final CommentRepository commentRepository; @Transactional public Comment insertCommnet(String content, int imageId, int userId) { //Tip: 객체를 만들때 가짜 객체를 만들어서 심플하게 id값만 insert 가능 Image image = new Image(); image.setId(imageId); User user = new User(); user.setId(userId); Comment comment = new Comment(); comment.setContent(content); comment.setImage(image); comment.setUser(user); return commentRepository.save(comment); }
하지만 단점으로는 RETURN값으로는 가짜객체에서 원하는 값만 들고온다는 점이다.여기서 CMRespDto의 장점이 나온다. 필요한 데이터가 다 들어와 있다 - 여기서 문제는 user가 댓글을 달기때문에 유저의 ID와 USERNAME 둘다 필요 하다는 것이다. 즉 가짜객체로 처리가 안된다는 말이다. 다시 코드를 짜보자.
@Transactional public Comment insertCommnet(String content, int imageId, int userId) { //Tip: 객체를 만들때 가짜 객체를 만들어서 심플하게 id값만 insert 가능 Image image = new Image(); image.setId(imageId); User userEntity = userRepository.findById(userId).orElseThrow(()->{ throw new CustomApiException("유저 아이디를 찾을 수 없습니다."); }); Comment comment = new Comment(); comment.setContent(content); comment.setImage(image); comment.setUser(userEntity); return commentRepository.save(comment); }
이제 유저의 모든 값을 다 들고 온다. 즉 필요한 유저값을 찾아서 뿌릴 수 있다는 말!
또한 처음에는 네이티브쿼리로 만들어 객체를 받아서 (User mSave ...) 진행하려했으나 객체타입으로는 스프링부트에서 받지 않는다. 그렇기 때문에 부트가 지원하는 save로 받아 처리한다. - 데이터는 잘 들고 왔지만 유저의 'images'는 필요없는 항목이니 Comment.java에 @JsonIgnoreProperties를 걸어주자
04 댓글 뷰 렌더링
- story.js 에서 addComment() 함수 마무리
function addComment(imageId) { let commentInput = $(`#storyCommentInput-${imageId}`); //내가 쓰는 댓글 let commentList = $(`#storyCommentList-${imageId}`);// 쓰는 댓글이 들어가는 리스트 let data = { imageId:imageId, content: commentInput.val() } if (data.content === "") { alert("댓글을 작성해주세요!"); return; } $.ajax({ type:"post", url:"/api/comment", data:JSON.stringify(data), contentType:"application/json; charset=utf-8", dataType:"json" }).done(res=>{ console.log("성공",res); let comment = res.data; let content = ` <div class="sl__item__contents__comment" id="storyCommentItem-${comment.id}"> <p> <b>${comment.user.username} :</b> ${comment.content} </p> <button><i class="fas fa-times"></i></button> </div> `; commentList.prepend(content); //prepend는 앞에 append는 뒤에 나열 }).fail(error=>{ console.log("오류",error); }); commentInput.val(""); }
- 작동이 잘 된다. 하지만 F5로 눌리면 댓글이 다시 사라진다. 이제 프로필 페이지 로드시에 이 함수를 불러서 같이 들고 오도록 세팅을 해야한다. 즉 ImageApiController에서 imageStroy()함수를 불러올때 댓글도 같이 불러오도록 세팅을 해줘야한다. Image.java에 댓글용 변수를 추가하도록 하자. 여기에 무한참조까지 방지를 해주자. 또한 네이티브 쿼리가 아닌 부트에서 지원하는 쿼리의 정렬은 @OrderBy로 처리한다.
public class Image { ... @OrderBy("id DESC") @JsonIgnoreProperties({"image"}) @OneToMany(mappedBy = "image") private List<Comment> comments; ... }
- 이제 story.js로 돌아가 댓글을 뿌리면 된다.