반응형

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이름을 넣었다 뺐대 함
   }
}
반응형

우리가 뽑아야 할 정보는 
1. 유저정보
2. 구독했는지 여부
3. 동일인인지 여부 - test1으로 로그인했는데 팔로우 정보에 test1이 있을때 팔로우 버튼이 안보이게 해야 하므로

시나리오
test1으로 로그인 했고 test2의 팔로우 정보를 보려한다. 
그럴때 아래와 같이 1) test2가 본인(test1)을 팔로 했을때는 팔로우관련 버튼이 안보이게 하고
2) test1의 팔로우 상황에 따라 다른 유저 팔로우 버튼을 달리 보이게 해야한다. 

 


현재 2번 페이지의 주인(fromUserId) 는 1번과 3번을 팔로우 하고 있기 때문에
팔로우 모달에서는 1번 유저와, 3번 유저의 정보가 나와야한다. 

그러기 위해선 user 테이블과 follow테이블이 조인해야한다. 
(user.id = follow.toUserId)

 

조인쿼리-
-> 이렇게 하면 모든 정보가 가져와지기 때문에

유저아이디, 유제이름, 유저의 프로플 사진 경로 정보만 가져온다.

 

 1. followState 컬럼 관련 쿼리작성 - 팔로우 여부 확인

그 다음에는 로그인한 유저(1번)이 3번을 팔로우 했는지 여부를 확인해야한다. (1번은 자기 자신이기 때문에 확인할 필요 X)
-> 팔로우 한다면 1번 컬럼에 1이 나온다.



그래서 이 결과를 아까의 조인 컬럼 옆에다 붙여야한다. 
id 1 열은 자기 자신이니까 아무 정보가 없어야하고 id 3열에는 팔로우 하고 있으니까 숫자 1이 있어야 하겠다. 

 

그러기 위해서는 스칼라 서브쿼리가 필요하다. 
스칼라 서브쿼리: select 절 안에 select문 있는것!
스칼라 서브쿼리는 단일행을 리턴해야 한다. 예컨대 아래와 같은 스칼라쿼리는 단일이 아닌 여러 행를 반환하므로 오류가 발생한다. 


followState 컬럼에는 로그인한 유저(여기선 1번)이 1번 유저를 팔로우 하는지, 또 3번 유저를 팔로우 하는지
에 관한 정보를 표기해야한다. 
따라서 스칼라서브쿼리에서  아래의 SELECT문(1번 유저가 특정 유저를 팔로하는지 확인하는 쿼리)을 복붙해서 
넣어주고 toUserId에 변수로 u.id를 넣어주면 된다.

결과

 

 2. 관련 쿼리작성 - 동일 유저 확인

다음으로 조회된 유저들이  로그인 유저(fromUserId)와  같은지를 판단하는 쿼리 및 컬럼을 만들어야한다. 
첫째열은 동일 유저니까 1이 나오는게 맞지만 둘째열은 아니니까 0으로 나와야한다. 

(1 = u.id) : 만약 로그인한 아이디(1)과 조회된 id가 같으면 1, 다르면 0을 표기한다.
*마리아db에서는 '==' 을 '=' 으로 쓴다. 

followState컬럼과 equalUserState컬럼의 값이 결과가 true면 1,아니면 0만 나오도록 if 문으로 감싼다. 
->최종 완료

===============================================

이렇게 만들어진 쿼리는 
FollowRepositoy 에 작성이 안된다. 왜냐하면 FollowRepositoy 의 반환타입은 Follow 모델인데 결과같은 Follow모델이 아니기 때문이다.
(public interface FollowRepositoy extends JpaRepository<Follow,Integer>{})

따라서 
FollowService에서 직접 네이티브 쿼리로 짜줘야한다. 

FollowService

- EntityManager를 DI해야한다. 
  모든 Repository 는 EntityManager를 구현해서 만들어져 있는 구현체이기 때문에 

1) 쿼리준비단계
StringBuffer로 한 줄씩 쿼리를 append해준다. 
여기서 주의해야할 점은 마지막 한 칸은 꼭 비워줘야한다. 안 띄워주면 다음 줄의 쿼리가 바로 붙어버린다.
또한 마지막에 세미콜론은 첨부하면 안된다.

2) 쿼리 완성단계
 1.물음표: principalId
 2.물음표 : 로그인한 아이디, principalId
 3. 물음표 : 현재 페이지 주인 아이디, pageUserId

3) 쿼리 실행단계
result.list(query, FollowDto.class);
: 쿼리 결과가 1건이 아니므로 resut.list메소드를 사용해준다. (단건이라면 uniqueResult 사용)
  매개변수로는 쿼리와 반환받을 타입(dto)를 넣어준다. 

JpaResultMapper 는 qlrm 라이브러리로부터 import 받아온건데,
qlrm이란 db에서 result된 결과를 자바클래스(dto)에 매핑해주는 라이브러리
내가 리턴받을 결과가 모델이아닌, 새로운 조합의 데이터면, dto라면 네이티브쿼리를 써야한다. jpa 못쓴다.

@RequiredArgsConstructor
@Service
public class FollowService {

    private final FollowRepositoy followRepositoy;
    private final EntityManager em;

    @Transactional(readOnly = true)
    public List<FollowDto> followList(int principalId, int pageUserId){
        //쿼리 준비
        StringBuffer sb = new StringBuffer();
        sb.append("SELECT u.id, u.username, u.profileImageUrl, ");
        sb.append("if((SELECT 1 FROM follow WHERE fromUserId = ? AND toUserId = u.id), 1, 0) followState, ");
        sb.append("if((? = u.id), 1, 0)equalUserState ");
        sb.append("FROM user u INNER JOIN follow f ");
        sb.append("ON u.id = f.toUserId ");
        sb.append("WHERE f.fromUserId = ? "); //세미콜론 첨부하면 안됨

        //쿼리 완성
        Query query = em.createNativeQuery(sb.toString())
                .setParameter(1, principalId)
                .setParameter(2, principalId)
                .setParameter(3, pageUserId);

        //1.물음표: principalId
        //2.물음표  :principalId
        //3. 물음표 :pageUserId

        JpaResultMapper result = new JpaResultMapper();
        List<FollowDto> followDtos = result.list(query, FollowDto.class);//한건을 받을게 아니니까

        return  followDtos;

    }
...
}
반응형

profile.html
- '팔로우 정보'를 클릭할때 subscribeInfoModalOpen(id) 함수가 실행되면서 해당 아이디의 팔로우 정보를 보이게할거다.

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


....
<div class="modal-subscribe">
  <div class="subscribe">
    <div class="subscribe-header">
      <span>팔로우정보</span>
      <button onclick="modalClose()">
        <i class="fas fa-times"></i>
      </button>
    </div>

    <div class="subscribe-list" id="subscribeModalList">
    
    </div>
  </div>
</div>

profile.js

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

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

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

           });

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

   });
}

모달에는 팔로우 대상자의 사진과 username, 팔로우 여부, 혹은 본인 여부에 따른 버튼 노출 설정이 필요하다

 

이런 정보들을 다 포괄할수 있는 data transform object가 필요하다.

FollowDto
id
: 만약 1이 로그인했는데, 1이  팔로우 정보 리스트에서 누군가를 팔로우, 혹은 언팔해야 하기 때문에 현재 로그인한 사용자의 아이디 뿐만 아니라 toUserId가 필요하다. 
username: 화면에 보일 username
followState: 팔로우 여부
equalUserState: 로그인한 유저와 팔로우 중인 유저정보가 동일인인지, false면 구독하기 버튼 보여주고 true면 아무 버튼 노출되면 안된다.

//follow정보를 보는 dto
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Data
public class FollowDto {

    //팔로우(언팔)할 대상의 아이디
    private int id;
    private String username;
    private String profileImageUrl;
    private Integer followState; //팔로우 여부
    private Integer equalUserState; //로그인한 유저와 팔로우중인 유저가 같은지

}

 

UserApiController

- followList() 메소드:
 특정 페이지 주인이 팔로우 하고 있는 모든 정보를 get하는 api
 현재 페이지의 id정보 및 로그인 유저의 id를 매개변수로 넘겨준다. 

@RequiredArgsConstructor
@RestController
public class UserApiController {

    private final UserService userService;
    private final FollowService followService;

    @GetMapping("api/user/{pageUserId}/follow")
    public ResponseEntity<?>followList(@PathVariable int pageUserId,  
    @AuthenticationPrincipal PrincipalDetails principalDetails){

        List<FollowDto> followDto = 
        followService.followList(principalDetails.getUser().getId(), pageUserId);

        return new ResponseEntity<>(new CMRespDto<>(1, "팔로우 정보 리스트 가져오기 성공", followDto), HttpStatus.OK);
    }

...


}

 

FollowService
-DB에 셀랙해서 팔로우 정보 모달에 보일 내용을 FollowDto형태로 가져와야한다. 이부분은 복잡하므로 다음에 이어 작성한다. 

@RequiredArgsConstructor
@Service
public class FollowService {

    private final FollowRepositoy followRepositoy;   

    @Transactional(readOnly = true)
    public List<FollowDto> followList(int principalId, int pageUserId){
     
     ...

        return  followDtos;

    }

...
}

+ Recent posts