- 이번 시간에는 스토리 페이지에서 자신이 구독한 사용자의 글에 좋아요를 마킹할 수 있는 기능을 구현해보겠습니다.
📝 Like
- 우선 아래와 같이 좋아요 객체를 구현하였습니다.
package com.cos.photogram.domain.likes;
...
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Table( //데이터베이스에서 두 개의 컬럼에 대해 unique 제약조건 설정
uniqueConstraints = {
@UniqueConstraint(
name = "likes_uk",
columnNames = {"image_id", "user_id"}
)
}
)
@Entity
public class Likes {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@JoinColumn(name = "image_id")
@ManyToOne
private Image image;
@JsonIgnoreProperties({"images"})
@JoinColumn(name = "user_id")
@ManyToOne
private User user;
private LocalDateTime create_date;
}
- 한 명의 유저가 하나의 글을 두 번 이상 좋아요 할 수 없도록 하기 위해
@Table
어노테이션을 사용하여 제약조건을 설정해주었습니다. Image
객체의 경우 양방향 매핑을 통해 좋아요 정보를 함께 가져오게 되는데 이때 무한 참조가 발생하지 않도록@JsonIgnoreProperties
어노테이션을 적절하게 사용해주었습니다.
package com.cos.photogram.domain.image;
...
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Entity
public class Image {
...
@JsonIgnoreProperties({"image"})
@OneToMany(mappedBy = "image")
private List<Likes> likes;
@Transient //데이터베이스에 컬럼을 생성하지 않음
private boolean likesState;
@Transient
private int likesCount;
...
}
- 여기서
likeState
와likesCount
변수는 각각 좋아요 상태와 좋아요 개수를 저장하는 변수로써, 이후 View 페이지로 해당 데이터를 전달하여 사용하기 위해 선언해주었습니다.
@Transient 어노테이션을 사용하면 엔티티에 변수를 추가하여도 데이터베이스에 컬럼을 생성하지 않습니다.
📝 Repository
- 이후 네이티브 쿼리를 통해 데이터베이스에 해당 데이터를 저장하고 삭제하는 쿼리를 작성해주었습니다.
package com.cos.photogram.domain.likes;
...
public interface LikesRepository extends JpaRepository<Likes, Long>{
@Modifying
@Query(value = "INSERT INTO likes(image_id, user_id, create_date) VALUES(:image_id, :principal_id, now())", nativeQuery = true)
int likes(Long image_id, Long principal_id);
@Modifying
@Query(value = "DELETE FROM likes WHERE image_id = :image_id AND user_id = :principal_id", nativeQuery = true)
int unLikes(Long image_id, Long principal_id);
}
- 각각의 함수는 좋아요와 좋아요 취소를 담당합니다.
📝 Service
- 서비스 단에서는 위에서 만든 함수를 사용하여 비즈니스 로직을 수행합니다.
package com.cos.photogram.service;
...
@RequiredArgsConstructor
@Service
public class LikesService {
private final LikesRepository likesRepository;
@Transactional
public void likes(Long imageId, Long principalId) {
likesRepository.likes(imageId, principalId);
}
@Transactional
public void unLikes(Long imageId, Long principalId) {
likesRepository.unLikes(imageId, principalId);
}
}
- 위의 비즈니스 로직은 사용자가 좋아요 버튼을 클릭할 시 발생하는 로직으로써 동작합니다.
- 이와 별개로 사용자가 로그인을 하고 스토리 페이지로 진입할 경우 좋아요 개수와 현재 접속한 사용자의 좋아요 상태를 체크하여 해당 데이터를 알맞게 View 페이지로 뿌려주어야 하는데, 이를 위해 기존에 구현하였던
ImageService
클래스의story()
함수를 아래와 같이 수정해주었습니다.
package com.cos.photogram.service;
...
@RequiredArgsConstructor
@Service
public class ImageService {
private final ImageRepository imageRepository;
...
@Transactional(readOnly = true)
public Page<Image> story(Long principalId, Pageable pageable) {
Page<Image> images = imageRepository.story(principalId, pageable);
images.forEach((image) -> {
image.setLikesCount(image.getLikes().size());
image.getLikes().forEach((like) -> {
if(like.getUser().getId() == principalId)
image.setLikesState(true);
});
});
return images;
}
}
📝 Controller
- 컨트롤러에서는 각각의 함수를 Post와 Delete 요청으로 전달받아 알맞은 요청을 수행합니다.
package com.cos.photogram.web.api;
...
@RequiredArgsConstructor
@RestController
public class ImageApiController {
private final LikesService likesService;
...
@PostMapping("/api/image/{imageId}/likes")
public ResponseEntity<?> likes(@PathVariable Long imageId, @AuthenticationPrincipal PrincipalDetails principalDetails) {
likesService.likes(imageId, principalDetails.getUser().getId());
return new ResponseEntity<>(new CMRespDto<>(1, "좋아요 성공", null), HttpStatus.CREATED);
}
@DeleteMapping("/api/image/{imageId}/likes")
public ResponseEntity<?> unLikes(@PathVariable Long imageId, @AuthenticationPrincipal PrincipalDetails principalDetails) {
likesService.unLikes(imageId, principalDetails.getUser().getId());
return new ResponseEntity<>(new CMRespDto<>(1, "좋아요 취소 성공", null), HttpStatus.OK);
}
}
📝 View
- 마지막으로 View 페이지의 일부를 아래와 같이 수정해주었습니다.
function getStoryItem(image) {
let item = `<div class="story-list__item">
<div class="sl__item__header">
<div>
<img class="profile-image" src="/upload/${image.user.profile_image_url}"
onerror="this.src='/images/person.jpeg'" />
</div>
<div>${image.user.username}</div>
</div>
<div class="sl__item__img">
<img src="/upload/${image.post_image_url}" />
</div>
<div class="sl__item__contents">
<div class="sl__item__contents__icon">
<button>`;
if(image.likesState)
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.likesCount}</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(() => {
let checkScroll = $(window).scrollTop() - ($(document).height() - $(window).height());
if(checkScroll > 0){
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(resp => {
$(`#storyLikeCount-${imageId}`).text(Number($(`#storyLikeCount-${imageId}`).text()) + 1);
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(resp => {
$(`#storyLikeCount-${imageId}`).text(Number($(`#storyLikeCount-${imageId}`).text()) - 1);
likeIcon.removeClass("fas");
likeIcon.removeClass("active");
likeIcon.addClass("far");
}).fail(error => {
console.log(error);
});
}
}
📝 Result
- 이후 결과를 살펴보면 좋아요 버튼을 클릭할 경우 아이콘이 알맞게 변경되며 실시간으로 개수가 카운팅 되는 것을 확인할 수 있습니다.
'🚗 Backend Toy Project > Photogram' 카테고리의 다른 글
[Photogram] 프로필 사진 변경 (0) | 2022.07.16 |
---|---|
[Photogram] 인기 페이지 구현 (2) | 2022.07.16 |
[Photogram] 스토리 페이지 (0) | 2022.07.14 |
[Photogram] 구독하기 - 구독 모달 (0) | 2022.07.14 |
[Photogram] 구독하기 - 기능 구현 (0) | 2022.07.12 |