포토그램

[Chapter 10] 댓글 구현하기

yukuda 2024. 2. 25. 20:53
728x90

74. 댓글-Comment 모델 만들기

Comment 모델 만들기

Comment 레파지토리 만들기

Comment 서비스 만들기

Comment API 컨트롤러 만들기

 

댓글 쓰기

 

댓글 삭제

 

com.cos.photogramstart.domain.comment.Comment.java

package com.cos.photogramstart.domain.comment;

import java.time.LocalDateTime;
import java.util.List;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.PrePersist;

import com.cos.photogramstart.domain.image.Image;
import com.cos.photogramstart.domain.likes.Likes;
import com.cos.photogramstart.domain.user.User;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Builder
@Entity // DB에 테이블을 생성
@Data
@NoArgsConstructor // 빈 생성자
@AllArgsConstructor // 전체 생성자
public class Comment {
	
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY) // 번호 증가 전략이 데이터베이스를 따라간다.
	private int id;
	
	@Column(length=100,nullable=false)
	private String content;
	
	@JoinColumn(name="userId")
	@ManyToOne(fetch=FetchType.EAGER)
	private User user;
	
	@JoinColumn(name="imageId")
	@ManyToOne(fetch=FetchType.EAGER)
	private Image image;
	
	private LocalDateTime createDate;
	@PrePersist // DB에 Insert가 되기 직전에 실행
	public void createDate() {
		this.createDate = LocalDateTime.now();
	}
}

com.cos.photogramstart.domain.comment.CommentRepository.java

package com.cos.photogramstart.domain.comment;

import org.springframework.data.jpa.repository.JpaRepository;

public interface CommentRepository extends JpaRepository<Comment, Integer>{

}

75. 댓글-컨트롤러, 서비스 만들기

com.cos.photogramstart.service.CommentService.java

package com.cos.photogramstart.service;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.cos.photogramstart.domain.comment.Comment;
import com.cos.photogramstart.domain.comment.CommentRepository;

import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@Service
public class CommentService {

	private final CommentRepository commentRepository;
	
	@Transactional
	public Comment 댓글쓰기() {
		return null;
	}
	
	@Transactional
	public void 댓글삭제() {
		
	}
}

com.cos.photogramstart.web.api.CommentApiController.java

package com.cos.photogramstart.web.api;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

import com.cos.photogramstart.service.CommentService;

import lombok.RequiredArgsConstructor;

@RestController
@RequiredArgsConstructor
public class CommentApiController {

	private final CommentService commentService;
	
	@PostMapping("/api/comment")
	public ResponseEntity<?> commentSave() {
		
		return null;
	}
	
	@DeleteMapping("/api/comment/{id}")
	public ResponseEntity<?> commentDelete(@PathVariable int id) {
		return null;
	}
}

76. 댓글-댓글쓰기 Ajax 함수 만들기

story.js

// (4) 댓글쓰기
function addComment(imageId) {

	let commentInput = $(`#storyCommentInput-${imageId}`);
	let commentList = $(`#storyCommentList-${imageId}`);

	let data = {
		imagaId: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);
	}).fail(error=>{
		console.log("오류",error);
	})

	let content = `
			  <div class="sl__item__contents__comment" id="storyCommentItem-2""> 
				<p>
				  <b>GilDong :</b>
				  댓글 샘플입니다.
				</p>
				<button><i class="fas fa-times"></i></button>
			  </div>
	`;
	commentList.prepend(content); // 최신 댓글
	commentInput.val("");
}

77. 댓글-댓글쓰기 완료

CommitDto.java

package com.cos.photogramstart.web.dto.comment;

import lombok.Data;

@Data
public class CommentDto {

	private String content;
	private int imageId;
	
	// toEntity가 필요없음
}

CommentApiController.java

package com.cos.photogramstart.web.api;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import com.cos.photogramstart.config.auth.PrincipalDatails;
import com.cos.photogramstart.domain.comment.Comment;
import com.cos.photogramstart.service.CommentService;
import com.cos.photogramstart.web.dto.CMRespDto;
import com.cos.photogramstart.web.dto.comment.CommentDto;

import lombok.RequiredArgsConstructor;

@RestController
@RequiredArgsConstructor
public class CommentApiController {

	private final CommentService commentService;
	
	@PostMapping("/api/comment")
	public ResponseEntity<?> commentSave(@RequestBody CommentDto commentDto,
			@AuthenticationPrincipal PrincipalDatails principalDatails ) {  // x-www-~ 뭐시기 안받고 JSON 받으려면 @RequestBody 붙여야함
		
		Comment comment = commentService.댓글쓰기(commentDto.getContent(),commentDto.getImageId(),principalDatails.getUser().getId()); // content, imageId, userId
		return new ResponseEntity<>(new CMRespDto<>(1,"댓글쓰기성공",comment),HttpStatus.CREATED);
	}
	
	@DeleteMapping("/api/comment/{id}")
	public ResponseEntity<?> commentDelete(@PathVariable int id) {
		return null;
	}
}

CommentService.java

package com.cos.photogramstart.service;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.cos.photogramstart.domain.comment.Comment;
import com.cos.photogramstart.domain.comment.CommentRepository;
import com.cos.photogramstart.domain.image.Image;
import com.cos.photogramstart.domain.user.User;
import com.cos.photogramstart.domain.user.UserRepository;
import com.cos.photogramstart.handler.ex.CustomApiException;

import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@Service
public class CommentService {

	private final CommentRepository commentRepository;
	private final UserRepository userRepository;
	
	@Transactional
	public Comment 댓글쓰기(String content, int imageId, int userId) {
		
		// Tip (객체를 만들 때 id값만 담아서 insert 할 수 있다.)
		// 대신 return 시에 image객체와 user객체는 id값만 가지고 있는 빈 객체를 리턴받는다.
		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);
	}
	
	@Transactional
	public void 댓글삭제() {
		
	}
}

78. 댓글-뷰 렌더링

Comment.java

@JoinColumn(name="imageId")
	@ManyToOne(fetch=FetchType.EAGER)
	private Image image;

story.js

<div id="storyCommentList-${image.id}">`;

			image.comments.forEach((comment)=>{
				item += `<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>`;
				
			});

		itme += `



$.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); // 최신 댓글
	
	}).fail(error=>{
		console.log("오류",error);
	})

	commentInput.val(""); // 인풋 필드를 깨끗하게 비워준다.

ImageApiController.java

@GetMapping("/api/image")
	public ResponseEntity<?> imageStory(@AuthenticationPrincipal PrincipalDatails principalDatails,
			@PageableDefault(size=3) Pageable pageable){
		Page<Image> images = imageService.이미지스토리(principalDatails.getUser().getId(), pageable);
		return new ResponseEntity<>(new CMRespDto<>(1, "성공",images),HttpStatus.OK); // JS에서 호출해야함
	}

Image.java

// 댓글
	@OrderBy("id DESC")
	@JsonIgnoreProperties({"image"})
	@OneToMany(mappedBy = "image")
	private List<Comment> comments;

79. 댓글-댓글 삭제하기

story.js

// (0) 현재 로그인한 사용자 아이디
let principalId = $("#principalId").val();

// (5) 댓글 삭제
function deleteComment(commentId) {
	$.ajax({
		type:"delete",
		url:`/api/comment/${commentId}`,
		dataType:"json"
	}).done(res=>{
		console.log("성공",res);
		$(`#storyCommentItem-${commentId`).remove();
	}).fail(error=>{
		console.log("성공",error);
	});
}

CommentApiController.java

package com.cos.photogramstart.web.api;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import com.cos.photogramstart.config.auth.PrincipalDatails;
import com.cos.photogramstart.domain.comment.Comment;
import com.cos.photogramstart.service.CommentService;
import com.cos.photogramstart.web.dto.CMRespDto;
import com.cos.photogramstart.web.dto.comment.CommentDto;

import lombok.RequiredArgsConstructor;

@RestController
@RequiredArgsConstructor
public class CommentApiController {

	private final CommentService commentService;
	
	@PostMapping("/api/comment")
	public ResponseEntity<?> commentSave(@RequestBody CommentDto commentDto,
			@AuthenticationPrincipal PrincipalDatails principalDatails ) {  // x-www-~ 뭐시기 안받고 JSON 받으려면 @RequestBody 붙여야함
		
		Comment comment = commentService.댓글쓰기(commentDto.getContent(),commentDto.getImageId(),principalDatails.getUser().getId()); // content, imageId, userId
		return new ResponseEntity<>(new CMRespDto<>(1,"댓글쓰기성공",comment),HttpStatus.CREATED);
	}
	
	@DeleteMapping("/api/comment/{id}")
	public ResponseEntity<?> commentDelete(@PathVariable int id) {
		commentService.댓글삭제(id);
		return new ResponseEntity<>(new CMRespDto<>(1,"댓글삭제성공",null),HttpStatus.OK);
	}
}

CommentService.java

@Transactional
	public void 댓글삭제(int id) {
		try {
			commentRepository.deleteById(id);			
		} catch (Exception e) {
			throw new CustomApiException(e.getMessage());
		}

	}

80. 댓글-유효성 검사하기

CommentDto.java

package com.cos.photogramstart.web.dto.comment;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;

import lombok.Data;

// NotNull = Null값 체크
// NotEmpty = 빈 값이거나 null 체크
// NotBlank = 빈 값이거나 null 체크 그리고 빈 공백(스페이스)까지

@Data
public class CommentDto {
	@NotBlank // 빈 값이거나 null 체크 그리고 빈 공백까지
	private String content;
	
	@NotNull // null 갑 체크
	private Integer imageId;
	
	// toEntity가 필요없음
}

CommentApiController.java

package com.cos.photogramstart.web.api;

import java.util.HashMap;
import java.util.Map;

import javax.validation.Valid;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import com.cos.photogramstart.config.auth.PrincipalDatails;
import com.cos.photogramstart.domain.comment.Comment;
import com.cos.photogramstart.handler.ex.CustomValidationApiException;
import com.cos.photogramstart.service.CommentService;
import com.cos.photogramstart.web.dto.CMRespDto;
import com.cos.photogramstart.web.dto.comment.CommentDto;

import lombok.RequiredArgsConstructor;

@RestController
@RequiredArgsConstructor
public class CommentApiController {

	private final CommentService commentService;
	
	@PostMapping("/api/comment")
	public ResponseEntity<?> commentSave(@Valid @RequestBody CommentDto commentDto, BindingResult bindingResult,
			@AuthenticationPrincipal PrincipalDatails principalDatails ) {  // x-www-~ 뭐시기 안받고 JSON 받으려면 @RequestBody 붙여야함
		
		if(bindingResult.hasErrors()) {
			Map<String, String> errorMap = new HashMap<>();
			
			for(FieldError error : bindingResult.getFieldErrors()) {
				errorMap.put(error.getField(),error.getDefaultMessage());
				// System.out.println(error.getDefaultMessage());
			}
			throw new CustomValidationApiException("유효성 검사 실패함", errorMap);
		}
		
		Comment comment = commentService.댓글쓰기(commentDto.getContent(),commentDto.getImageId(),principalDatails.getUser().getId()); // content, imageId, userId
		return new ResponseEntity<>(new CMRespDto<>(1,"댓글쓰기성공",comment),HttpStatus.CREATED);
	}
	
	@DeleteMapping("/api/comment/{id}")
	public ResponseEntity<?> commentDelete(@PathVariable int id) {
		commentService.댓글삭제(id);
		return new ResponseEntity<>(new CMRespDto<>(1,"댓글삭제성공",null),HttpStatus.OK);
	}
}

UserApiController.java

package com.cos.photogramstart.web.api;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.validation.Valid;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.web.bind.annotation.AuthenticationPrincipal;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import com.cos.photogramstart.config.auth.PrincipalDatails;
import com.cos.photogramstart.domain.user.User;
import com.cos.photogramstart.handler.ex.CustomValidationApiException;
import com.cos.photogramstart.handler.ex.CustomValidationException;
import com.cos.photogramstart.service.SubscribeService;
import com.cos.photogramstart.service.UserService;
import com.cos.photogramstart.web.dto.CMRespDto;
import com.cos.photogramstart.web.dto.subscribe.SubscribeDto;
import com.cos.photogramstart.web.dto.user.UserUpdateDto;

import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@RestController
public class UserApiController {

	private final UserService userService;
	private final SubscribeService subscribeService;
	
	@PutMapping("/api/user/{principalId}/profileImageUrl")
	public ResponseEntity<?> profileImageUrlUpdate(@PathVariable int principalId, MultipartFile profileImageFile, @AuthenticationPrincipal PrincipalDatails principalDatails) {
		User userEntity = userService.회원프로필사진변경(principalId, profileImageFile);
		principalDatails.setUser(userEntity); // 세션 변경
		return new ResponseEntity<>(new CMRespDto<>(1, "프로필사진변경 성공",null), HttpStatus.OK);
	}
	
	@GetMapping("/api/user/{pageUserId}/subscribe")
	public ResponseEntity<?> subscribeList(@PathVariable int pageUserId, @AuthenticationPrincipal PrincipalDatails principalDatails){
		
		List<SubscribeDto> subscribeDto = subscribeService.구독리스트(principalDatails.getUser().getId(),pageUserId);
		
		return new ResponseEntity<>(new CMRespDto<>(1,"구독자 정보 리스트 가져오기 성공",subscribeDto),HttpStatus.OK);
	}
	
	@PutMapping("/api/user/{id}")
	public CMRespDto<?> update(
			@PathVariable int id, 
			@Valid UserUpdateDto userUpdateDto, 
			BindingResult bindingResult, // 꼭 @Valid가 적혀 있는 다음 파라미터에 적어야함
			@AuthenticationPrincipal PrincipalDatails principalDatails) {
		
		if(bindingResult.hasErrors()) {
			Map<String, String> errorMap = new HashMap<>();
			
			for(FieldError error : bindingResult.getFieldErrors()) {
				errorMap.put(error.getField(),error.getDefaultMessage());
				// System.out.println(error.getDefaultMessage());
			}
			throw new CustomValidationApiException("유효성 검사 실패함", errorMap);
		}else {
			// System.out.println(userUpdateDto);
			User userEntity = userService.회원수정(id, userUpdateDto.toEntity());
			
			principalDatails.setUser(userEntity); // 세션 정보 변경(바뀌면 '남'이 뜨게 (gender))
			
			return new CMRespDto<>(1,"회원수정완료",userEntity); // 응답시에 userEntity의 모든 getter 함수가 호출되고 JSON으로
															 // 파싱하여 응답한다.
		}
	}
}

story.js

$.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 onclick="deleteComment(${conment.id})"><i class="fas fa-times"></i></button>
			
		  </div>
	`;
	commentList.prepend(content); // 최신 댓글
	
	}).fail(error=>{
		console.log("오류",error.responseJson.data.content);
		alert(error.responseJson.data.content);
	})

	commentInput.val(""); // 인풋 필드를 깨끗하게 비워준다.