[Photogram] 좋아요 구현하기

2022. 7. 15. 11:39·🚗 Backend Toy Project/Photogram
  • 이번 시간에는 스토리 페이지에서 자신이 구독한 사용자의 글에 좋아요를 마킹할 수 있는 기능을 구현해보겠습니다.


📝 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

  • 이후 결과를 살펴보면 좋아요 버튼을 클릭할 경우 아이콘이 알맞게 변경되며 실시간으로 개수가 카운팅 되는 것을 확인할 수 있습니다.

 

GitHub - Daegwon-Kim/SpringBoot-Photogram

Contribute to Daegwon-Kim/SpringBoot-Photogram development by creating an account on GitHub.

github.com

 

저작자표시 (새창열림)

'🚗 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
'🚗 Backend Toy Project/Photogram' 카테고리의 다른 글
  • [Photogram] 프로필 사진 변경
  • [Photogram] 인기 페이지 구현
  • [Photogram] 스토리 페이지
  • [Photogram] 구독하기 - 구독 모달
Baeg-won
Baeg-won
  • Baeg-won
    좋았다면 추억이고 나빴다면 경험이다.
    Baeg-won
  • 전체
    오늘
    어제
    • 분류 전체보기
      • 🍃 Spring, Spring Boot
        • 스프링 프레임워크 기초
        • 스프링 핵심 원리 - 기본편
        • 자바 ORM 표준 JPA 프로그래밍 - 기본편
        • 스프링 MVC
        • 실전! 스프링 부트와 JPA 활용1 - 웹 애플리..
      • 🥑 Web Technoloy
      • 🚗 Backend Toy Project
        • 스프링 부트 게시판
        • Photogram
        • Baeg-won Clothing Gallery
      • 🥇 Problem Solving
        • Breadth-First Search
        • Depth-First Search
        • Backtracking
        • Simulation
        • Two-pointer
        • Binary Search
        • Greedy
        • Dynamic Programming
        • Minimum Spanning Tree
        • Dijkstra
        • Floyd warshall
      • ☕ Java
        • 명품 자바 에센셜
        • Applications
      • 🍦 JavaScript
        • JavaScript 기초
      • 🐧 Linux
        • 이것이 리눅스다(CentOS 8)
      • 📟 Database
        • 혼자 공부하는 SQL
      • 🧬 Data Structure
      • 🎬 HTML
      • 🎤 Tech Interview
      • 📌 etc
        • Unity 2D Raising Jelly Game
        • C++
        • 영어 쉐도잉
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
Baeg-won
[Photogram] 좋아요 구현하기
상단으로

티스토리툴바