Wanna be Brilliant Full-Stack Developer

2/17 SpringBoot Likes View Rendering 본문

Back-End/Spring Boot

2/17 SpringBoot Likes View Rendering

Flashpacker 2022. 2. 17. 13:03


좋아요를 누를떄마다

<button>
<i class="fas fa-heart active" id="storyLikeIcon-${image.id}" onclick="toggleLike(${image.id})"></i>
</button> 이부분이 active가 

이렇게 바뀐다. 내가 알아야할것이 머냐면 좋아요 했는지 안했는지 상태에 대해 알아야한다! 

그 정보를 이미지Api컨트롤러에서 이미지스토리 가져올떄 images에 담아가야한다!

 

내가 이미지를 셀렉트 할떄 좋아요 정보를 가져올라면 양방형 매핑을 해야한다! 

	//이미지 좋아요
	@OneToMany(mappedBy = "image")
	private List<Likes> likes;

이렇게 걸어주면 우리가 이미지를 셀렉트할때 Like정보를 같이 가져올것이다.

 

이미지 하나가 들고 있는 like정보를 가지고 이미지서비스에서 작업해야한다! 

우리가 뭘찾아내야하냐면 2번(cos)에 로그인하고 2번이 구독하고 있는 images 쭉 가져왔고 

그 이미지를 for문을 돌려서 하나씩 뽑아내자, 첫번쨰 이미지를 뽑아내면 그 이미지를 좋아요하는 정보를 뽑아낼것이다.

그 좋아요를 내 자신 2번이 좋아한것인지 확인해야한다. 

 

 

package com.cos.photogramstart.service;

import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.UUID;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.cos.photogramstart.config.auth.PrincipalDetails;
import com.cos.photogramstart.domain.image.Image;
import com.cos.photogramstart.domain.image.ImageRepository;
import com.cos.photogramstart.web.dto.image.ImageUploadDto;

import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@Service		
public class ImageService {
	
	private final ImageRepository imageRepository;
	
	@Transactional(readOnly = true) // 영속성 컨텍스트 변경 감지를 해서, 더티체킹, flush(반영) 
	public Page<Image> 이미지스토리(int principalId, Pageable pageable) {
		Page<Image> images = imageRepository.mStory(principalId, pageable);
		
		//2(cos) 로그인
		//images에 좋아요 상태 담기
		images.forEach((image)-> {
			
			image.getLikes().forEach((like) -> {
				if(like.getUser().getId() == principalId) { // 해당 이미지에 좋아요한 사람들을 찾아서 현재로그인 한사람이 좋아요 한것인지 비교
					image.setLikeState(true);
				}
			});
			
		});
		
		return images;
	}
	
	
	@Value("${file.path}")
	private String uploadFolder;
	
	@Transactional
	public void 사진업로드(ImageUploadDto imageUploadDto, PrincipalDetails principalDetails) {
		UUID uuid  = UUID.randomUUID();	 // uuid
		String imageFileName = uuid+ "_" + imageUploadDto.getFile().getOriginalFilename(); // 1.jpg
		System.out.println("이미지 파일이름: "+ imageFileName);
		
		Path imageFilePath = Paths.get(uploadFolder+imageFileName);
		
		//통신, I/O -> 예외 
		try {
			Files.write(imageFilePath, imageUploadDto.getFile().getBytes());
		} catch (Exception e) {
			e.printStackTrace();
		}
		
		// image 테이블에 저장
		Image image = imageUploadDto.toEntity(imageFileName, principalDetails.getUser()); // acc21c4e-947a-475d-9220-6117b0d4efa8_BB1eWj3u.jfif 
		 imageRepository.save(image);
		
		//System.out.println(imageEntity.toString());
	}
}
package com.cos.photogramstart.domain.image;

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

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.OneToMany;
import javax.persistence.PrePersist;
import javax.persistence.Transient;

import com.cos.photogramstart.domain.likes.Likes;
import com.cos.photogramstart.domain.subscribe.Subscribe;
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
@AllArgsConstructor
@NoArgsConstructor
@Data
@Entity
public class Image { // N, 1
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY) 
	private int id;
	private String caption; // 설명 
	private String postImageUrl; // 사진을 전송받아서 그 사진을 서버에 특정 폴더에 저장 - DB에 그 저장된 경로를 insert\

	@JsonIgnoreProperties({"images"})
	@JoinColumn(name = "userId")
	@ManyToOne(fetch = FetchType.EAGER) // 이미지를 select하면 조인해서 User정보를 같이들고옴
	private User user;// 1, 1
	
	//이미지 좋아요
	@JsonIgnoreProperties({"image"})
	@OneToMany(mappedBy = "image")
	private List<Likes> likes;
	
	// 댓글 
	
	private LocalDateTime createDate;
	
	@Transient //DB에 칼럼이 만들어지지 않는다.
	private boolean likeState;
	
	@PrePersist
	public void createDate() {
		this.createDate = LocalDateTime.now();
	}

		// 오브젝트를 콘솔에 출력할 때 문제가 될 수 있어서 User 부분을 출력 되지 않게 함.
//	@Override
//	public String toString() {
//		return "Image [id=" + id + ", caption=" + caption + ", postImageUrl=" + postImageUrl 
//				+ ", createDate=" + createDate + "]";
//	}

}

if문을 사용해서 story.js에 좋아요를 누르면 하트 ,아니면 비어져있게 만들것이다.

/**
	2. 스토리 페이지
	(1) 스토리 로드하기
	(2) 스토리 스크롤 페이징하기
	(3) 좋아요, 안좋아요
	(4) 댓글쓰기
	(5) 댓글삭제
 */

// (1) 스토리 로드하기
let page = 0;

function storyLoad() {
	$.ajax({
		url: `/api/image?page=${page}`,
		dataType: "json"
	}).done(res=> {
		console.log(res);
		res.data.content.forEach((image)=>{
			let storyItem = getStoryItem(image);
			$("#storyList").append(storyItem);
		});
	}).fail(error=>{
		console.log("오류",error);
	});
}

storyLoad();

function getStoryItem(image) {
	let item =`<div class="story-list__item">
	<div class="sl__item__header">
		<div>
			<img class="profile-image" src="/upload/${image.user.profileImageUrl}"
				onerror="this.src='/images/person.jpeg'" />
		</div>
		<div>${image.user.username}</div>
	</div>

	<div class="sl__item__img">
		<img src="/upload/${image.postImageUrl}" />
	</div>

	<div class="sl__item__contents">
		<div class="sl__item__contents__icon">

			<button>`;

				if(image.likeState) {
			item +=`	<i class="fas fa-heart active" id="storyLikeIcon-${image.id}" onclick="toggleLike(${image.id})"></i>`;
				}else {
					item +=`	<i class="far fa-heart" id="storyLikeIcon-${image.id}" onclick="toggleLike(${image.id})"></i>`;
				}
				
				
				item += `
	</button>
</div>

<span class="like"><b id="storyLikeCount-1">3 </b>likes</span>

<div class="sl__item__contents__content">
	<p>${image.caption}</p>
</div>

<div id="storyCommentList-1">

	<div class="sl__item__contents__comment" id="storyCommentItem-1"">
		<p>
			<b>Lovely :</b> 부럽습니다.
		</p>

		<button>
			<i class="fas fa-times"></i>
		</button>

	</div>

</div>

<div class="sl__item__input">
	<input type="text" placeholder="댓글 달기..." id="storyCommentInput-1" />
	<button type="button" onClick="addComment()">게시</button>
		</div>

	</div>
</div>`;
	return item;
}

// (2) 스토리 스크롤 페이징하기
$(window).scroll(() => {
	//console.log("윈도우 scrollTop",$(window).scrollTop());
	//console.log("문서의 높이",$(document).height());
	//console.log("윈도우 높이",$(window).height());
	
	let checkNum = $(window).scrollTop() - ($(document).height() - $(window).height() );
	console.log(checkNum);
	
	if(checkNum <10 && checkNum > -10){
			page++;
			storyLoad();
	}
});


// (3) 좋아요, 안좋아요
function toggleLike(imageId) {
	let likeIcon = $(`#storyLikeIcon-${imageId}`);
	if (likeIcon.hasClass("far")) {
		likeIcon.addClass("fas");
		likeIcon.addClass("active");
		likeIcon.removeClass("far");
	} else {
		likeIcon.removeClass("fas");
		likeIcon.removeClass("active");
		likeIcon.addClass("far");
	}
}

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

	let commentInput = $("#storyCommentInput-1");
	let commentList = $("#storyCommentList-1");

	let data = {
		content: commentInput.val()
	}

	if (data.content === "") {
		alert("댓글을 작성해주세요!");
		return;
	}

	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("");
}

// (5) 댓글 삭제
function deleteComment() {

}

좋아요 카운트 뷰 렌더링하기!

이미지에 가서 Transient를 하나 더만든다 DB에 칼럼만들지말라고!

@Transient
private int likeCount;

또 이미지 서비스에 가서 좋아요 카운트를 담을것이다.

image.setLikeCount(image.getLikes().size());

image에 like카운트 정보를 넣어주기만 하면된다. 

story.js에서 

<span class="like"><b id="storyLikeCount-${image.id}">${image.likeCount} </b>likes</span>

클릭을 하면 실제로 좋아요가 실행이 되야한다

애클릭하는 이벤트를 찾아보겠다! 

좋아요를 검사 하면 toggleLike가 나온다

// (3) 좋아요, 안좋아요
function toggleLike(imageId) {
	let likeIcon = $(`#storyLikeIcon-${imageId}`);
	
	if (likeIcon.hasClass("far")) {
	
	
		likeIcon.addClass("fas");
		likeIcon.addClass("active");
		likeIcon.removeClass("far");
	} else {
		
		
		likeIcon.removeClass("fas");
		likeIcon.removeClass("active");
		likeIcon.addClass("far");
	}
}

여기서 class가 "far"라는건 far는 빈껍데기 하트고 fas가 빨간하트다. 

/**
	2. 스토리 페이지
	(1) 스토리 로드하기
	(2) 스토리 스크롤 페이징하기
	(3) 좋아요, 안좋아요
	(4) 댓글쓰기
	(5) 댓글삭제
 */

// (1) 스토리 로드하기
let page = 0;

function storyLoad() {
	$.ajax({
		url: `/api/image?page=${page}`,
		dataType: "json"
	}).done(res=> {
		console.log(res);
		res.data.content.forEach((image)=>{
			let storyItem = getStoryItem(image);
			$("#storyList").append(storyItem);
		});
	}).fail(error=>{
		console.log("오류",error);
	});
}

storyLoad();

function getStoryItem(image) {
	let item =`<div class="story-list__item">
	<div class="sl__item__header">
		<div>
			<img class="profile-image" src="/upload/${image.user.profileImageUrl}"
				onerror="this.src='/images/person.jpeg'" />
		</div>
		<div>${image.user.username}</div>
	</div>

	<div class="sl__item__img">
		<img src="/upload/${image.postImageUrl}" />
	</div>

	<div class="sl__item__contents">
		<div class="sl__item__contents__icon">

			<button>`;

				if(image.likeState) {
			item +=`	<i class="fas fa-heart active" id="storyLikeIcon-${image.id}" onclick="toggleLike(${image.id})"></i>`;
				}else {
					item +=`	<i class="far fa-heart" id="storyLikeIcon-${image.id}" onclick="toggleLike(${image.id})"></i>`;
				}
				
				
				item += `
	</button>
</div>

<span class="like"><b id="storyLikeCount-${image.id}">${image.likeCount} </b>likes</span>

<div class="sl__item__contents__content">
	<p>${image.caption}</p>
</div>

<div id="storyCommentList-1">

	<div class="sl__item__contents__comment" id="storyCommentItem-1"">
		<p>
			<b>Lovely :</b> 부럽습니다.
		</p>

		<button>
			<i class="fas fa-times"></i>
		</button>

	</div>

</div>

<div class="sl__item__input">
	<input type="text" placeholder="댓글 달기..." id="storyCommentInput-1" />
	<button type="button" onClick="addComment()">게시</button>
		</div>

	</div>
</div>`;
	return item;
}

// (2) 스토리 스크롤 페이징하기
$(window).scroll(() => {
	//console.log("윈도우 scrollTop",$(window).scrollTop());
	//console.log("문서의 높이",$(document).height());
	//console.log("윈도우 높이",$(window).height());
	
	let checkNum = $(window).scrollTop() - ($(document).height() - $(window).height() );
	console.log(checkNum);
	
	if(checkNum <1 && checkNum > -1){
			page++;
			storyLoad();
	}
});


// (3) 좋아요, 안좋아요
function toggleLike(imageId) {
	let likeIcon = $(`#storyLikeIcon-${imageId}`);
	
	if (likeIcon.hasClass("far")) { // 좋아요 하겠다 
	
	$.ajax({
		type:"post",
		url:`/api/image/${imageId}/likes`,
		dataType: "json"
	}).done(res=> {
		likeIcon.addClass("fas");
		likeIcon.addClass("active");
		likeIcon.removeClass("far");
	}).fail(error=>{
		console.log("오류",error);
	});
	
	} else { // 좋아요를 취소하겠다 
		
			$.ajax({
		type:"delete",
		url:`/api/image/${imageId}/likes`,
		dataType: "json"
	}).done(res=> {
		likeIcon.removeClass("fas");
		likeIcon.removeClass("active");
		likeIcon.addClass("far");
	}).fail(error=>{
		console.log("오류",error);
	});
		

	}
}

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

	let commentInput = $("#storyCommentInput-1");
	let commentList = $("#storyCommentList-1");

	let data = {
		content: commentInput.val()
	}

	if (data.content === "") {
		alert("댓글을 작성해주세요!");
		return;
	}

	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("");
}

// (5) 댓글 삭제
function deleteComment() {

}

지금 이렇게 하니 좋아요를 누르면 DB에는 데이터가 들어가지만 화면에서는 동적으로 보이지 않아서 새로고침을 해야 좋아요의 개수가 올라가는것을 볼 수 있다. 

 

/**
	2. 스토리 페이지
	(1) 스토리 로드하기
	(2) 스토리 스크롤 페이징하기
	(3) 좋아요, 안좋아요
	(4) 댓글쓰기
	(5) 댓글삭제
 */

// (1) 스토리 로드하기
let page = 0;

function storyLoad() {
	$.ajax({
		url: `/api/image?page=${page}`,
		dataType: "json"
	}).done(res=> {
		console.log(res);
		res.data.content.forEach((image)=>{
			let storyItem = getStoryItem(image);
			$("#storyList").append(storyItem);
		});
	}).fail(error=>{
		console.log("오류",error);
	});
}

storyLoad();

function getStoryItem(image) {
	let item =`<div class="story-list__item">
	<div class="sl__item__header">
		<div>
			<img class="profile-image" src="/upload/${image.user.profileImageUrl}"
				onerror="this.src='/images/person.jpeg'" />
		</div>
		<div>${image.user.username}</div>
	</div>

	<div class="sl__item__img">
		<img src="/upload/${image.postImageUrl}" />
	</div>

	<div class="sl__item__contents">
		<div class="sl__item__contents__icon">

			<button>`;

				if(image.likeState) {
			item +=`	<i class="fas fa-heart active" id="storyLikeIcon-${image.id}" onclick="toggleLike(${image.id})"></i>`;
				}else {
					item +=`	<i class="far fa-heart" id="storyLikeIcon-${image.id}" onclick="toggleLike(${image.id})"></i>`;
				}
				
				
				item += `
	</button>
</div>

<span class="like"><b id="storyLikeCount-${image.id}">${image.likeCount} </b>likes</span>

<div class="sl__item__contents__content">
	<p>${image.caption}</p>
</div>

<div id="storyCommentList-1">

	<div class="sl__item__contents__comment" id="storyCommentItem-1"">
		<p>
			<b>Lovely :</b> 부럽습니다.
		</p>

		<button>
			<i class="fas fa-times"></i>
		</button>

	</div>

</div>

<div class="sl__item__input">
	<input type="text" placeholder="댓글 달기..." id="storyCommentInput-1" />
	<button type="button" onClick="addComment()">게시</button>
		</div>

	</div>
</div>`;
	return item;
}

// (2) 스토리 스크롤 페이징하기
$(window).scroll(() => {
	//console.log("윈도우 scrollTop",$(window).scrollTop());
	//console.log("문서의 높이",$(document).height());
	//console.log("윈도우 높이",$(window).height());
	
	let checkNum = $(window).scrollTop() - ($(document).height() - $(window).height() );
	
	
	if(checkNum <1 && checkNum > -1){
			page++;
			storyLoad();
	}
});


// (3) 좋아요, 안좋아요
function toggleLike(imageId) {
	let likeIcon = $(`#storyLikeIcon-${imageId}`);
	
	if (likeIcon.hasClass("far")) { // 좋아요 하겠다 
	
	$.ajax({
		type:"post",
		url:`/api/image/${imageId}/likes`,
		dataType: "json"
	}).done(res=> {
		
		let likeCountStr = $(`#storyLikeCount-${imageId}`).text();
		let likeCount = Number(likeCountStr) + 1;
	   $(`#storyLikeCount-${imageId}`).text(likeCount);
		
		likeIcon.addClass("fas");
		likeIcon.addClass("active");
		likeIcon.removeClass("far");
	}).fail(error=>{
		console.log("오류",error);
	});
	
	} else { // 좋아요를 취소하겠다 
		
			$.ajax({
		type:"delete",
		url:`/api/image/${imageId}/likes`,
		dataType: "json"
	}).done(res=> {
		
	   let likeCountStr = $(`#storyLikeCount-${imageId}`).text();
		let likeCount = Number(likeCountStr) - 1;
	   $(`#storyLikeCount-${imageId}`).text(likeCount);
		
		likeIcon.removeClass("fas");
		likeIcon.removeClass("active");
		likeIcon.addClass("far");
	}).fail(error=>{
		console.log("오류",error);
	});
		

	}
}

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

	let commentInput = $("#storyCommentInput-1");
	let commentList = $("#storyCommentList-1");

	let data = {
		content: commentInput.val()
	}

	if (data.content === "") {
		alert("댓글을 작성해주세요!");
		return;
	}

	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("");
}

// (5) 댓글 삭제
function deleteComment() {

}

이렇게 좋아요가 카운트가 적용되었다! 

 


우리가 무한 참조 버그를 잡기 위해서는 연관관계에대해서도 알고있어야하는데!

Likes테이블은 누구랑 연관관계를 가지고있냐면 이미지와 유저랑 가지고 있다.

포린키는 Likes가 가지고 있다.  그러면 유저테이블이 먼저 삭제될수가 없다.

유저 테이블을 참고해서 Likes테이블이 만들어지고 image테이블을 참고해서 likes테이블이 또한 만들어지는데

이 삭제 순서는 유저테이블을 먼저 삭제하면 likes테이블은 삭제 하지 못한다.

삭제순서는 자식부터 likes부터 그다음에 유저테이블을 삭제하면된다. 

여기서 Jackson은 머냐면 자바 오브젝트를 JSON으로 파싱해주는 라이브러리이다.

우리가 따로 등록하지 않아도 스프링스타터 프로젝터에 다 가지고 있다. 

오류가 난 이유는 이미지를 선택하면 유저랑 like를 들고온다. like안에 유저가 있고 유저 안에 이미지가 또 있다. 이미지 안에 likes가 또 있고 like안에 또 유저가 있고 유저안에 이미지가 있고 이렇게무한참조가 되는데

이렇게 무한참조의 시작을 보면 이미지를 셀렉트했을때는문제가 없는데 like까지 나오는거는 문제 가없는데 likes안에서 문제 가없는데 유저안에 이미지가 튀어나올떄 문제가 생긴것이다. 

이게 무한반복되니까 무엇을 잡아야하냐면 

package com.cos.photogramstart.domain.likes;

import java.time.LocalDateTime;

import javax.persistence.Entity;
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 javax.persistence.Table;
import javax.persistence.UniqueConstraint;

import com.cos.photogramstart.domain.image.Image;
import com.cos.photogramstart.domain.subscribe.Subscribe;
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
@AllArgsConstructor
@NoArgsConstructor
@Data
@Entity
@Table(
		uniqueConstraints = {
				@UniqueConstraint(
						name="likes_uk",
						columnNames = {"imageId", "userId"}
						)
				
		}
		)
public class Likes { //N
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY) 
	private int id;
	
	//무한 참조됨
	@JoinColumn(name = "imageId")
	@ManyToOne
	private Image image; //1
	
	//오류가 터지고 나서 잡아봅시다.
	@JsonIgnoreProperties({"images"})
	@JoinColumn(name = "userId")
	@ManyToOne
	private User user; //1 
	
	private LocalDateTime createDate;
	
	@PrePersist
	public void createDate() {
		this.createDate = LocalDateTime.now();
	}
	
	
}

likes.java에서 

//오류가 터지고 나서 잡아봅시다.
@JsonIgnoreProperties({"images"})
@JoinColumn(name = "userId")
@ManyToOne
private User user; //1  

likes안에 images가 나오지 않으면 무한참조는 해결된다