반응형

우리가 뽑아야 할 정보는 
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;

    }

...
}
반응형

profile.html
- 이전에 followState에 따라 버튼에 어떤 메시지가 보일지를 세팅했다.
- 버튼을 클릭했을때 팔로우, 혹은 언팔로우 기능을 실행할 자바스크립트 코드를 삽입했다. 
- toggleSubscribe 를 클릭했을때 this를 전달하는데 여기서 this란 이벤트 정보를 의미한다. 

<div th:if="${dto.followState == true}">
  <button class="cta blue" th:onclick="toggleSubscribe([[${dto.user.id}]], this)">팔로우 취소</button>
</div>
<div th:if="${dto.followState == false}">
  <button class="cta" th:onclick="toggleSubscribe([[${dto.user.id}]],this)">팔로우</button>
</div>

 

profile.js
- toggleClass: 클래스의 css이름을 넣었다 뺐대 함
- toggleSubscribe는 toUserId(현재 페이지 주인의 id)와 obj를 받는다. 

1. 버튼이 '언팔로우'일때
정상적으로 끝나면 "팔로우" 와 클래스의 색을 blue로 표시하게 한다. 
2. 버튼이 '팔로우' 일때 
정상적으로 끝나면 "팔로우" 와 클래스의 색을 blue로 표시하게 한다. 

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);
            });
   }
}
반응형

UserProfileDto

followState: 팔로우를 한 상태인지
followCount: 팔로우수

@Builder
@AllArgsConstructor
@NoArgsConstructor
@Data
public class UserProfileDto {
    private boolean pageOwnerState;
    private int imageCount;
    private boolean followState;
    private int followCount;
    private User user;

}

 

FollowRepositoy

- select 문이기 때문에 @Modifying 어노테이션 필요 없음
- mFollowState:
로그인한 유저가 현재 접근한 페이지의 유저를 팔로우 했는지 여부를 구하는 쿼리다.
로그인한 계정id(principalId) 와 현재 접속한 페이지의 유저아이디(pageUserId)를 매개변수로 받는다.
- mFollowCount:
현재 접근한 페이지의 유저가 팔로우하는 수를 구하는 쿼리다. 
pageUserId를 매개변수로 받는다.

public interface FollowRepositoy extends JpaRepository<Follow,Integer>{

 ...
 
    //구독 여부(test1으로 로그인, test2 페이지로감 - 1번이 2번 팔로우 했는지 - 1나오면 구독한다는거 ), 1이면 팔로우한 상태
    @Query(value = "SELECT COUNT(*) FROM follow WHERE fromUserId = :principalId AND toUserId = :pageUserId", nativeQuery = true)
    int mFollowState(int principalId, int pageUserId);

    //해당 페이지의 유저가 팔로우 하는 수
    @Query(value = "SELECT COUNT(*) FROM follow WHERE fromUserId = :pageUserId", nativeQuery = true)
    int mFollowCount(int pageUserId);

}

 이 쿼리들은 UserController에서 "user/profile"로 갈때 쿼리로 얻어진 정보를 UserProfileDto에 담아서 가져갈거다. 

 

UserService

- FollowRepository 를 DI해서 가져온다. 
- 로그인한 유저의 페이지 주인 팔로우 여부(followState)와 페이지 주인의 팔로우 수(followCount)를 구해서
  dto에 담는다. 
- followState가 1이면 true, 아니면 false가 담겨진다.

@RequiredArgsConstructor
@Service
public class UserService {

    private final UserRepository userRepository;
    private final FollowRepositoy followRepositoy;
    private final BCryptPasswordEncoder bCryptPasswordEncoder;

    @Transactional(readOnly = true)
    public UserProfileDto userProfile(int pageUserId, int principalId){

        UserProfileDto dto = new UserProfileDto();

        //SELECT * FROM image WHERE USERiD = :userId;
        User userEntity = userRepository.findById(pageUserId).orElseThrow(() -> {
            throw new CustomException("해당 프로필 페이지는 없는 페이지입니다.");
        }); 

        dto.setUser(userEntity);
        //pageUserId 와 principalId가 같은지 비교
        dto.setPageOwnerState(pageUserId == principalId);
        dto.setImageCount(userEntity.getImages().size());

        int followState = followRepositoy.mFollowState(principalId, pageUserId);
        int followCount = followRepositoy.mFollowCount(pageUserId);

        dto.setFollowState(followState == 1);
        dto.setFollowCount(followCount);

        return dto;
    }
    
    ...

}
반응형

유저의 프로필 화면에 유저가 등록한 이미지들을 보이게 할거다.

Userservice

-userProfile 메소드는 해당페이지 유저의 사진을 반환한다.
- findById로 매개변수로 넘어온 유저아이디로 유저를 찾는다. 
  만약 유저정보가 없다면 orElseThrow로 예외를 발동하고,  CustomException을 throw한다. 

@RequiredArgsConstructor
@Service
public class UserService {

    private final UserRepository userRepository;
    private final BCryptPasswordEncoder bCryptPasswordEncoder;

    @Transactional(readOnly = true)
    public UserProfileDto userProfile(int pageUserId, int principalId){

        UserProfileDto dto = new UserProfileDto();

        //SELECT * FROM image WHERE USERiD = :userId;
        User userEntity = userRepository.findById(pageUserId).orElseThrow(() -> {
            throw new CustomException("해당 프로필 페이지는 없는 페이지입니다.");
        }); 

        dto.setUser(userEntity);
        //pageUserId 와 principalId가 같은지 비교
        dto.setPageOwnerState(pageUserId == pageUserId);
        dto.setImageCount(userEntity.getImages().size());


        return dto;
    }

  ...
}

 

CustomException
- 에러 메시지만 받도록 한다. 

public class CustomException extends RuntimeException{

    private static final long serialVersionUID = 1L;

    public CustomException(String message){
        super(message);
    }

}

 

ControllerExceptionHandler

@RestController
@ControllerAdvice //모든 Exception들을 낚아챔
public class ControllerExceptionHandler {

...
    @ExceptionHandler(CustomException.class) //CustomValidationException 발동하는 모든 Exception을 이 함수가 가로챔
    public String exception(CustomException e){
       return  Script.back(e.getMessage());
    }

...
}

 

UserController

- user/profile 로 유저 정보 뿐만 아니라 유저와 연관된 이미지, 팔로우 정보도 가져가야 한다. 
- 그래서 user셀렉 할때 이미지 정보 등을 가져오게 해야하고, 이는 양방향 매핑으로 구현 가능하다.

@RequiredArgsConstructor
@Controller
public class UserController {

    private final UserService userService;

    @GetMapping({"/user/{pageUserId}"})
    public String profile(@PathVariable int pageUserId, Model model,
                          @AuthenticationPrincipal PrincipalDetails principalDetails){

        UserProfileDto dto = userService.userProfile(pageUserId, principalDetails.getUser().getId());

        model.addAttribute("dto", dto);
        return "user/profile";
    }

......
}

 

User
- mappedyBy:
1. 나는 연관관계의 주인이 아니다. 그러므로 테이블에 컬럼을 만들지마
2. User를 select할때 해당 유저 Id로 등록된 image들을 다 가져와

- OneToMany에서는 FetchType.LAZY이 기본 옵션
 1. Lazy = User를 SELECT할때 해당 유저 ID로 등록된 image들을 가져오지마
   대신 getImages()함수의 image들이호출될때 가져와!
 2. Eager = User를 SELECT할때 해당 유저 ID로 등록된 image들을 전부 Join해서 가져와!

@Builder
@AllArgsConstructor
@NoArgsConstructor  //Bean생성자
@Data
@Entity //DB에 테이블 생성
public class User {
    ...
 
    @OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
    @JsonIgnoreProperties({"user"}) //Image내부에 있는 user를 무시하고 파싱한다.
    private List<Image> images;

  ...
}

+ Recent posts