반응형

목표: like가 많은수대로 내림차순으로 이미지를 보이게 할거다.

ImageController.class

@RequiredArgsConstructor
@Controller
public class ImageController {

    private final ImageService imageService;
    private final UserService userService;

...
    @GetMapping({"/image/popular"})
    public String popular(Model model){

        List<Image> images =imageService.popularImg();
        model.addAttribute("images", images);
        return "image/popular";
    }

 ...

}

 

ImageService.class

@RequiredArgsConstructor
@Service
public class ImageService {

    private final ImageRepository imageRepository;

    @Transactional(readOnly = true)
    public List<Image>popularImg(){

        return imageRepository.mPopular();
    }

...
}

 

ImageRepository

public interface ImageRepository extends JpaRepository<Image, Integer> {

...

    @Query(value = "SELECT i.* FROM image i INNER JOIN(SELECT imageId, COUNT(imageId) likeCount FROM likes GROUP BY imageId) c ON i.id = c.imageId ORDER BY likeCount DESC", nativeQuery = true)
    List<Image> mPopular();
...

}

이미지 인기순 구하는 쿼리짜기

likes 테이블에 보면 3번 이미지는 2개의, 2번 이미지는 1개의 like을 받은 것을 알 수 있다.

따라서  전체 이미지 중 3번-> 2번 순으로 2개의 이미지만 구해져야한다. 

likes 테이블에 imageId 와 likeCount라는 가상컬럼을 만든다.

 

imageId가 중복으로 나오므로 imageId를 기준으로 GROUP BY  해서 묶어준다.

imageId의 수 만큼을 구해져야 함으로 imageId 를 기준으로 COUNT 해주고 이 결과를 DESC해서 조회한다. 

이렇게 구해진 쿼리를 위의 IN에 넣으려 하면 오류가 생긴다.
IN에는 ID값만 들어가야 하는데 방금 구한 쿼리는 likeCount 값도 포함되있기 때문이다.

 

이때 인라인 뷰를 활용한다.
인라인 뷰란 FROM 절에서 사용하는 서브쿼리로 ,
FROM 절에서 서브쿼리 자체가 하나의 테이블처럼 사용된다.

그렇게 만들어진 인라인 뷰를
WHERE IN 절에 넣었는데, 내리차순이 적용안됐다!

 

사실 IN연산자에 3,2 로 순서를 바꿔서 넣어도 오름차순으로 반영되서 조회되기 때문에 
IN 연산자로 해결할수 없다. 
따라서 다른 방법으로 JOIN해야한다. 

그래서 아래 SELECT절로 구해진 imageId와 위 SELECT절의 id 값가 같은것만 조인한다.

따라서 아래와 같이 inner join한다.

 

== 완성==
ImageRepository 에서는 쿼리의 반환타입이 Image 클래스이기 때문에 image 테이블이 가지고 있는 값만 구한다. 

 

popular.html
타임리프 반복문을 통해 이미지 정보를 노출시킨다.

...
        <!--인기게시글 갤러리(GRID배치)-->
        <div class="popular-gallery">

            <tr th:each="image : ${images}">

                <div class="p-img-box">
                    <a href="/user/${image.user.id}" th:href="|/user/${image.user.id}|" >
                        <img src="/images/home.jpg" th:src="${image.postImageUrl}" />
                    </a>
                </div>
            </tr>
        </div>
...
반응형

story.js
-image.likeState에 따라 이미지태그의 active 가 보일지를 정해야한다. 

function getStoryItem(image) {
    let item =
    `<div class="story-list__item">
  ...
 <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>
 
 <span class="like"><b id="storyLikeCount-${image.id}">${image.likeCount} </b>likes</span>


...

    return item

}

 

그 정보는 storyLoad()함수에서 '/api/image' api에 응답하여 done을 통해 image를 가져올때 담아가야한다. 

function storyLoad() {
    $.ajax({
        url:`/api/image`,
        ataType : "json"
    }).done(res=>{
        console.log(res);
        res.data.forEach((image) => {
            let storyItem = getStoryItem(image)
            $("#storyList").append(storyItem);

        })
    }).fail(error => {
        console.log("오류", error);

    });
}

storyLoad();

 

Image.class
- Image를 가져올때 like정보를 가져오려면 Like과의 연관관계가 필요하다. 
- OneToMany 는 lazy 로딩이 기본이므로 likes의 getter를 호출하면 이미지의 like정보를 가져올 수 있다. 
- boolean타입으로 likeState 만들어준다. 이때 @Transient 어노테이션으로 db에 컬럼생성 막아준다. 
- 또한 좋아요 수 를 구하는 likeCount를  @Transient 어노테이션으로 만들어 준다. 

@Builder
@AllArgsConstructor
@NoArgsConstructor
@Data
@Entity
public class Image {

    ...

    //이미지 좋아요 정보
    @JsonIgnoreProperties({"image"})
    @OneToMany(mappedBy = "image") //lazy가 기본
    private List<Likes>likes;
    
   @Transient //DB에 컬럼이 만들어지지 않는다.
    private boolean likeState;

    @Transient
    private int likeCount;
   ...
    }

ImageService.class
 
예컨데 '2'로 로그인 한다고 하자
-> 2가 팔로우한 계정의 이미지롤 for문으로 모두 가져오기
-> 각 이미지의 좋아요 정보를 모두 가져와서
-> 그 좋아요가 내가 좋아요 한지 판별하기(image의 like정보의 user와 principalId가 같은지 보기)
    해당 이미지에 좋아요한 사람들을 찾아서 현재 로긴한 사람이 좋아요 한것인지 비교
-> 만약에 같다면 likeState에 true값 넣기

- 또한 setLikeCount 로 좋아요 수를 images에 넣어준다.

@RequiredArgsConstructor
@Service
public class ImageService {

    private final ImageRepository imageRepository;

    @Transactional(readOnly = true)
    public List<Image>imageStory(int principalId){
        List<Image> images = imageRepository.mStory(principalId);
     
        images.forEach((image)->{

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

            image.getLikes().forEach((like) -> {
                if(like.getUser().getId() == principalId){ 
                    image.setLikeState(true);
                }
            });
        });

       return images;
    }
   ...
    }
}

  

story.js

// (3) 좋아요, 안좋아요
function toggleLike(imageId) {
   let likeIcon = $(`#storyLikeIcon-${imageId}`);

   if (likeIcon.hasClass("far")) { //빈하트-> LIKE하겠다

       $.ajax({
                 type: "post",
                 url: `/api/image/${imageId}/likes`,
                 dataType: "json"
              }).done(res=>{
				//b태그 내용의 text부분을 가져온다
                 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 {  //빨간하트 ->UNLIKE 하겠다.

        $.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);
              });

   }
}
반응형

Likes.class
-mysql과 마리아db는 like가 키워드라서 Likes란 이름으로 짓는다.
-한 유저가 특정 이미지를 중복으로 like할 수 없기때문에 @Table - uniqueConstraints 설정을 해준다.
- Likes와 Images는 n:1 관계다 
- 또한  likes와 User 도 n:1관계다 

@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 //기본 패치전략은 EAGER
    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();
    }
}

 

LikesRepository
- likes 테이블에 이미지 아이디와 로그인한 유저아이디, 그리고 현재시간을 넣어주는 쿼리작성

public interface LikesRepository extends JpaRepository<Likes, Integer> {

    @Modifying
    @Query(value = "INSERT INTO likes(imageId, userId, createDate) VALUES (:imageId, :principalId, now())", nativeQuery = true)
    int mLikes(int imageId, int principalId);


    @Modifying
    @Query(value = "DELETE FROM likes WHERE imageId =:imageId AND userId =:principalId", nativeQuery = true)
    int mUnLikes(int imageId, int principalId);
}

 

LikesService

@RequiredArgsConstructor
@Service
public class LikesService {

    private final LikesRepository likesRepository;

    @Transactional
    public void like(int imageId, int principalId){
        likesRepository.mLikes(imageId, principalId);
    }

    @Transactional
    public void unlike(int imageId, int principalId){
        likesRepository.mUnLikes(imageId, principalId);
    }

}


ImageApiController

@RequiredArgsConstructor
@RestController
public class ImageApiController {

    private final ImageService imageService;
    private final LikesService likesService;

   ...

    //이미지 좋아요
    @PostMapping("/api/image/{imageId}/likes")
    public ResponseEntity<?>likes(@PathVariable int imageId, @AuthenticationPrincipal PrincipalDetails principalDetails){

        likesService.like(imageId, principalDetails.getUser().getId());

        return new ResponseEntity<>(new CMRespDto<>(1, "좋아요성공", null), HttpStatus.CREATED);

    }

    //이미지 좋아요 취소
    @DeleteMapping("/api/image/{imageId}/likes")
    public ResponseEntity<?>unLikes(@PathVariable int imageId, @AuthenticationPrincipal PrincipalDetails principalDetails){

        likesService.unlike(imageId, principalDetails.getUser().getId());

        return new ResponseEntity<>(new CMRespDto<>(1, "좋아요 취소성공", null), HttpStatus.OK);

    }
}

 

반응형

메인화면에서 보일 페이지를 구성할거다.
유저정보, 이미지url, 캡션, like정보, 댓글 정보가 필요한데 일단 유저정보, 이미지정보, 캡션 정보만 보이게할거다.

시나리오:
2번으로 로그인해서 메인 페이지(피드)를 본다.

따라서 다음과 같이 userId가 1,3인 이미지 정보만 봐야한다.

IN 내부에 직접 적는건 무리가 있으니까
2번 follow 테이블에서 fromUserId(로그인한 유저)의 아이디가 팔로우 하는 유저의 정보를 가져오는 쿼리를 짜고

 

그쿼리를 아까 이미지를 SELECT하는 쿼리의 서브쿼리로 넣어준다.

 

Image.class
이미지를 조회했을때 User 객체의 image정보를 또 들고올 수 있으니까 
@JsonIgnoreProperties({"images"}) 를 통해 User의 image정보는 가져오지 않게 한다.

@Builder
@AllArgsConstructor
@NoArgsConstructor
@Data
@Entity
public class Image {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;

    private String caption;
    private String postImageUrl; //-Db에 그 저장될 이미지 명

    @JsonIgnoreProperties({"images"})
    @JoinColumn(name = "userId") // DB컬럼명 설정
    @ManyToOne(fetch = FetchType.EAGER)
    private User user;

    //이미지 좋아요 정보

    //댓글

    private LocalDateTime createDate;

    @PrePersist
    public void createDate(){
        this.createDate = LocalDateTime.now();
    }

 

ImageRepository

public interface ImageRepository extends JpaRepository<Image, Integer> {

    //로그인 한 사람이 자신을 제외한 다른 유저들의 사진을 구하는 쿼리
    @Query(value = "SELECT * FROM image WHERE userId IN (SELECT toUserId FROM follow WHERE fromUserId = :principalId) ORDER BY id DESC;", nativeQuery = true)
    List<Image> mStory(int principalId);

}

 

ImageApiController
-로그인 유저정보를 imageServicedml imageStory 메소드매개변수로 전달해준다.

@RequiredArgsConstructor
@RestController
public class ImageApiController {

    private final ImageService imageService;

    @GetMapping("/api/image")
    public ResponseEntity<?>imageStory(@AuthenticationPrincipal PrincipalDetails principalDetails
    ){
        List<Image> images = imageService.imageStory(principalDetails.getUser().getId());

        return new ResponseEntity<>(new CMRespDto<>(1, "성공", images), HttpStatus.OK);
    }

   

}

 

ImageService

@RequiredArgsConstructor
@Service
public class ImageService {

    private final ImageRepository imageRepository;

    @Transactional(readOnly = true)
    public List<Image>imageStory(int principalId){
        List<Image> images = imageRepository.mStory(principalId);

        return images;
    }

 ...
    }
}

 

story.html
- story.js를 로드한다.

<html xmlns:th="http://www.thymeleaf.org">

<!--<%@ include file="../layout/header.jsp"%>-->
<div th:replace="layout/header" :: menu />
<main class="main">
    <section class="container">
        <!--전체 리스트 시작-->
        <article class="story-list" id="storyList">

            <!--전체 리스트 아이템-->


        </article>
    </section>
</main>
<script src="/js/story.js"></script>
</body>
</html>

 

story.js
- url은 '/api/image' 로 요청
- 응답받은 결과를 for문 돌면서 getStoryItem() 함수의 매개변수로 넣어주고
 그 결과(storyItem)을 html id값 'storyList'에 append해 준다.

// (1) 스토리 로드하기
function storyLoad() {
    $.ajax({
        url:`/api/image`,
        ataType : "json"
    }).done(res=>{
        console.log(res);
        res.data.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="/${image.user.profileImageUrl}"
                      onerror="this.src='/images/person.jpeg'" />
             </div>
             <div>${image.user.username}</div>
         </div>

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

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

                 <button>
                     <i class="fas fa-heart active" id="storyLikeIcon-${image.id}" onclick="toggleLike(${image.id})"></i>
                 </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

}

 

반응형

profile.html
- 현재 페이지의 아이디를 subscribeInfoModalOpen함수에  전달해야한다.

<div class="subscribe">
        <ul>
          <li><a href=""> 게시물<span th:text="${dto.imageCount}">3</span>
          </a></li>
          <script  th:inline="javascript">
            /*<![CDATA[*/
            var id = "[[${dto.user.id}]]";
            /*]]>*/
          </script>

          <li>
            <a href="javascript:subscribeInfoModalOpen(id);">
              팔로우정보<span th:text="${dto.followCount} ">2</span>
            </a>
          </li>
        </ul>
      
 </div>

profile.js
요청 url 은 '/api/user/페이지유저아이디/follow
- 그렇게 얻어진 결과값인 res의 data를 forEach로 돌면서 유저정보u 를 getSubscribeModalItem() 함수에 넣어주고
  그 결과를 item 에 넣고 profile.html의 #subscribeModalList id 값을 가진 요소에 붙여준다.

// (2) 팔로우정보  모달 보기
function subscribeInfoModalOpen(pageUserId) {
//modal-subscribe라는 클래스를 찾아서 화면에 보여줌
   $(".modal-subscribe").css("display", "flex");

   $.ajax({
       url:`/api/user/${pageUserId}/follow`,
       dataType:"json"
   }).done(res=>{

        res.data.forEach((u) => {
            let item = getSubscribeModalItem(u);
            $("#subscribeModalList").append(item);

           });

   }).fail(error=>{
       console.log("구독정보 불러오기 오류", error);

   });
}

 

 if(!u.equalUserState):  동일유저가 아닐때 팔로우 관련 버튼이 보여야한다.
 if(u.followState) : 팔로우 상태에 따른 분기

function getSubscribeModalItem(u) {
let item =
`<div class="subscribe__item" id="subscribeModalItem-${u.id}">
     <div class="subscribe__img">
       <img src="/upload/${u.profileImageUrl}" onerror="this.src='/images/person.jpeg'"/>
     </div>
     <div class="subscribe__text">
       <h2>${u.username}</h2>
     </div> 
 <div class="subscribe__btn">`;

 if(!u.equalUserState){ 
    if(u.followState){ 
     item += `<button class="cta blue" onclick="toggleSubscribe(${u.id}, this)">팔로우취소</button>`
    }else{
     item += `<button class="cta" onclick="toggleSubscribe(${u.id},this)">팔로우</button>`
    }
 }

     item += `
     </div>
   </div>`;

    return item;
}

 

toggleSubscribe(): 팔로우(언팔) 함수

function toggleSubscribe(toUserId, obj) {
   if ($(obj).text() === "팔로우취소") {

       $.ajax({
        type:"delete",
        url: "/api/follow/"+toUserId,
        dataType: "json"
       }).done(res=>{
              $(obj).text("팔로우");
              $(obj).toggleClass("blue");
       }).fail(error=>{
            console.log("팔로우취소실패", error);
       });


   } else {

        $.ajax({
             type:"post",
             url: "/api/follow/"+toUserId,
             dataType: "json"
            }).done(res=>{
                $(obj).text("팔로우취소");
                $(obj).toggleClass("blue");
            }).fail(error=>{
                console.log("팔로우하기실패", error);
            });
//    toggle: 클래스의 css이름을 넣었다 뺐대 함
   }
}

+ Recent posts