Project/토이 프로젝트

[Cinemagram] 팔로우 기능 구현 - (9)

Lea Hwang 2022. 12. 26. 17:13

원래 이 기능은 가장 나중에 하려고 했습니다. 랜더링 하는 부분들을 구현한 후에 조금 복잡한 기능을 구현하는 게 정신건강에 좋을 것 같다는 판단이었습니다.

 

 

하지만 여러 테스트를 하던 와중 이와 같은 에러가 발생했습니다.

 

Postman으로 구독 API 테스트 中

ERROR UsernamePasswordAuthenticationFilter : An internal error occurred while trying to authenticate the user.

org.springframework.security.authentication.InternalAuthenticationServiceException: UserDetailsService returned null, which is an interface contract violation

 

해결방법에 대해 구글링을 아무리 해봐도 코드에 구멍 난 곳은 없다고 판단이 들었습니다.(해결하지 못했다는 뜻)

혹시 몰라 우선 팔로우 기능을 구현하고 브라우저, Postman 둘 다 테스트 해볼 생각으로 시작했습니다.

 

 

 

 


 

 

팔로우 기능 구현은 

  • 페이지에서 확인 가능한 부분
    • 구독 상태에 따른 팔로우, 언팔로우 버튼 구현
    • 구독자 수(팔로잉) 표기
  • 구독 정보 모달에서 확인 가능한 부분으로 나뉩니다.
    • 페이지 유저가 구독(팔로우)하고 있는 유저 정보
    • 로그인 한 유저 입장에서, 팔로우, 언팔로우 가능
      • 만약 로그인 한 유저가 모달안에 있을 시 나 자신 이므로 팔로우 관련 버튼 숨기기

 

 

 

첫 번째

페이지에서 확인 가능한 부분을 구현하도록 하겠습니다.

 

어떤 페이지일까요? profile.html입니다. 그럼 해당페이지로 데이터를 실어서 보낼 DTO를 생성해야 하는데요, 

우리는 이전에 UserProfileDto를 생성했습니다. 여기에 팔로우 상태, 팔로잉 수를 가져올 필드를 추가해 보겠습니다.

UserProfileDto

@Data
public class UserProfileDto {
    private boolean pageUserState;
    private int imageCount;
    private User user;

    private boolean followState;
    private int followCount; 
}

 

 

현재 DB상태를 보면(Postman에서 작업함)

 

 

 

이제 출력하고 자 하는 바는 명확해졌습니다. 바로 쿼리를 짜려고 하기보다 step by step으로 시나리오를 짜면서 최종 쿼리를 만들어보겠습니다. 

 

쿼리

1. 우리는 profile페이지에 구독자 수(팔로잉), 구독 상태의 데이터가 넘어가야 합니다.

2. API스펙에 맞는 UserProfileDto의 followState, followCount 부분입니다.

 

 

구독자 수 (팔로잉) 

현재 follow 테이블의 상태를 보겠습니다. 페이지 유저 아이디는 2번으로 가정하겠습니다.

SELECT * FROM follow;

 

SELECT COUNT(*) FROM follow WHERE fromUserId=2;
  • 구독자 ""이므로 리턴을 COUNT(*)로 했습니다.
  • fromUserId 부분은 어떤 유저의 구독자 수를 보고 싶은지에 따라 달라질 수 있습니다. 

 

 

구독 상태 → 구독 상태에 따른 팔로우, 언팔로우 버튼 구현

누구의 구독상태를 봐야 할까요? 

 

로그인 한 유저가 해당 페이지의 유저를 구독했는지를 봐야합니다.

SELECT COUNT(*) FROM follow WHERE fromUserId=1 AND toUserId=2;
  • COUNT(*)
    • fromUserId가 toUserId를 구독하고 있으면 1을 리턴, 아니면 0을 리턴합니다.
  • fromUserId = 로그인한 유저
  • toUserId = 페이지 유저

 

이를 통해 로그인한 유저가 페이지 이동을 하게 되면 버튼으로 확인 가능해졌습니다.

 

 

저 화면은 최종화면이고요, 이제 저 쿼리를 적용하고 호출하는 과정을 간략히 나열해 보겠습니다.

 

FollowRepository

@Query(value = "SELECT COUNT(*) FROM follow WHERE fromUserId = :sessionId AND toUserId = :pageUserId", nativeQuery = true)
int cFollowState(@Param(value = "sessionId") int sessionId, @Param(value = "pageUserId") int pageUserId);

@Query(value = "SELECT COUNT(*) FROM follow WHERE fromUserId = :pageUserId", nativeQuery = true)
int cFollowCount(@Param(value = "pageUserId") int pageUserId);
  • SELECT만 하는 것이므로 @Modifying은 생략했습니다.

 

UserController : 관련 Service에 필요한 정보를 넘기면서 호출합니다.

@GetMapping("/user/{pageUserId}")
public String profile(@PathVariable int pageUserId, Model model, @AuthenticationPrincipal CustomUserDetails customUserDetails) {
    UserProfileDto userProfileDto = userService.profile(pageUserId, customUserDetails.getUser().getId());
    model.addAttribute("dto", userProfileDto);
   
    return "user/profile";
}

 

UserService : 비즈니스 로직 - UserProfileDto의 setting 해주면 됩니다. 

@Transactional(readOnly = true)
public UserProfileDto profile(int pageUserId, int sessionId) {
    UserProfileDto dto = new UserProfileDto();

    ...

   int followState = followRepository.cFollowState(sessionId, pageUserId);
   int followCount = followRepository.cFollowCount(pageUserId);

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

    return dto;
}

 

 

버튼 눌렀을 때 팔로우, 언팔로우 바뀌는 부분은 Ajax 호출

FollowApiController

@PostMapping("/api/follow/{toUserId}")
    public ResponseEntity<?> follow(@AuthenticationPrincipal CustomUserDetails customUserDetails, @PathVariable int toUserId) {
        followService.follow(customUserDetails.getUser().getId(),toUserId);
        return new ResponseEntity<>(new ResDto<>(1,"팔로우 성공", null), HttpStatus.OK);
    }

@DeleteMapping("/api/follow/{toUserId}")
    public ResponseEntity<?> unFollow(@AuthenticationPrincipal CustomUserDetails customUserDetails, @PathVariable int toUserId) {
        followService.unFollow(customUserDetails.getUser().getId(), toUserId);
        return new ResponseEntity<>(new ResDto<>(1,"언팔로우 성공", null), HttpStatus.OK);
    }
}

 

 

 

 


 

 

 

두 번째

구독 정보 모달 클릭 시 어떤 데이터가 출력되어야 하는지를 구현해 보겠습니다.

 

구독 모달 정보 :  페이지의 유저가 구독하고 있는 유저들 정보
필요한 정보 : 유저 정보와 팔로우 정보

  • 유저 정보
    • id
    • username
    • profileImageUrl
  • 팔로우 정보
    • username :  페이지 유저가 구독한 유저의 username
    • 팔로우, 언팔로우 버튼 : 로그인 유저 입장에서, 팔로우했는지 안 했는지에 따라 다르게 구현
      • 만약, 로그인한 유저가 username에 있다면 나 자신이므로 구독 버튼 자체가 없어야 함

 

 

 

구독 정보 모달에 필요한 데이터를 출력할 수 있는 DTO를 생성합니다.

FollowInfoDto

public class FollowInfoDto {
    private Integer id;
    private String username;
    private String profileImageUrl;

    private Integer followState;
    private Integer equalUserState;
}

 

쿼리

  • 목표
    • 페이지유저가 구독하고 있는 유저*를 추출하고,
    • 로그인 한 유저가 그 유저*를 구독했는지 여부를 나타내는 쿼리 생성
  • 활용 개념
    • 조인
    • 스칼라 서브쿼리

 

 

Inner Join

목표 : 페이지 유저가 팔로우하고 있는 유저의 정보를 출력하는 쿼리 생성

가정 : apple(id:1)으로 로그인한 후 banana(id:2) 페이지로 이동

 

 

생각하는 흐름

1. User테이블의 정보와 Follow테이블의 정보가 필요함

2. 그럼 User테이블과 Follow의 테이블의 공통 칼럼은 무엇일까?

user.id = follow.toUserId

 

3. INNER JOIN 사용

SELECT * FROM user u INNER JOIN follow f
ON u.id = f.toUserId
WHERE f.fromUserId=2;

 

4. 하지만 여기서 모든 칼럼이 다 필요한 것은 아니다. id, username, profileImageUrl이 필요함

SELECT u.id, u.username, u.profileImageUrl
FROM user u INNER JOIN follow f
ON u.id = f.toUserId
WHERE f.fromUserId=2;

 

5. 그럼 빨간 박스 쿼리 구현은 끝났습니다.

 

 

가상 칼럼 추가, 스칼라 서브쿼리

목표 : 우리는 FollowInfoDto의 모든 필드를 출력해야 함을 잊어선 안됩니다. 

 

위의 쿼리를 통해 id, username, profileImageUrl은 끝났습니다.

추가적으로 followState, equalUserState만 옆에 칼럼을 붙이면 됩니다. (가상 컬럼 추가)

 

 

생각하는 흐름

1. 우선 가상 칼럼을 추가하는 방법을 알아야 합니다.

SELECT u.id, u.username, u.profileImageUrl, 10 followState
FROM user u INNER JOIN follow f
ON u.id = f.toUserId
WHERE f.fromUserId=2;
  • 10 : 값을 임의로 채움
  • followState 칼럼명

 

 

2. 스칼라 서브쿼리 - followState 완성

: SELECT절에 SELECT절이 또 들어가는 것 (반드시 단일 행을 리턴해야 함)

SELECT * FROM follow;

 

현재 1번 유저는 2,3번 유저를 팔로우하고 있습니다.

 

SELECT u.id, u.username, u.profileImageUrl, 
(SELECT true FROM follow WHERE fromUserId=1 AND toUserId= u.id) followState
FROM user u INNER JOIN follow f
ON u.id = f.toUserId
WHERE f.fromUserId=2;

  • fromUserId =1
    • 로그인 한 유저
  • toUserId=u.id
    • 변수 자리
    • 여기서는 페이지 유저가 팔로우하고 있는 유저를 의미 (1,3번)
  • WHERE f.fromUserId=2
    • 이동한 페이지 유저

 

3. null, false로 표현하는 것보단 0,1로 표현하는 게 좋으므로 수정

SELECT u.id, u.username, u.profileImageUrl, 
if((SELECT 1 FROM follow WHERE fromUserId=1 AND toUserId= u.id), 1, 0) followState
FROM user u INNER JOIN follow f
ON u.id = f.toUserId
WHERE f.fromUserId=2;

 

4. equalUserState칼럼 추가

SELECT u.id, u.username, u.profileImageUrl, 
if((SELECT 1 FROM follow WHERE fromUserId=1 AND toUserId= u.id), 1, 0) followState,
if((1=u.id), 1, 0) equalUserState
FROM user u INNER JOIN follow f
ON u.id = f.toUserId
WHERE f.fromUserId=2;

 

여기까지 해서 FollowInfoDto의 모든 필드를 출력할 수 있는 쿼리를 작성완료 했습니다. 

 

 

 

 

그럼 여태까지 해왔던 것처럼 관련 Repository에 native query로 작성해 보겠습니다.

하지만 이번에는 Repository에서 구현할 수 없습니다.

 

그 이유는 특정 Repository는 특정 타입(Follow 모델)만 리턴하기 때문입니다. (상속받은 클래스 타입의 모델만 리턴)

 

따라서 직접 짠 쿼리의 결과가 특정 타입(Follow 모델)이 아니라면 Repository에 nativeQuery를 넣을 수가 없습니다. 

  • 실습할 코드는 DTO를 리턴해야 함
  • 이처럼 DTO로 받아 내야 하는 쿼리의 경우 JpaRepository를 상속받은 인터페이스로 nativeQuery를 짜서 날릴 수 없다. 

 

이 부분은 QLRM라이브러리를 사용하여서 처리하였고 내용이 길어 따로 포스팅하였습니다. 혹시 궁금하신 분들은 참고하시면 될 것 같습니다. (관련 포스팅)

 

 

 

 

이제  Ajax를 구현해서 모달에 뿌리면 끝입니다.

 

버튼 부분 profile.html

<a th:onclick="javascript:subscribeInfoModalOpen([[ ${dto.user.id} ]])"> 팔로잉</a>

 

UserApiController에서 처리하도록 했습니다. 

@GetMapping("/api/user/{pageUserId}/follow")
public ResponseEntity<?> followList(@PathVariable int pageUserId, @AuthenticationPrincipal CustomUserDetails customUserDetails) {
    List<FollowInfoDto> followInfoDto = followService.followInfoList(customUserDetails.getUser().getId(), pageUserId);

    return new ResponseEntity<>(new ResDto<>(1, "구독자 정보 리스트 불러오기 성공", followInfoDto), HttpStatus.OK);

}

 

 

 

 


 

😁 정리

팔로우 기능 페이지 확인)

  • 로그인 후(1번 유저), 프로필 페이지로 이동
    • 팔로우 관련 버튼 없고 이미지 추가 버튼 
    • 팔로잉에 로그인한 유저가 팔로우 중인 유저 수가 보임
  • 다른 페이지 이동
    • 로그인한 유저가 이미 팔로우 중이면 언팔로우 버튼이
    • 로그인한 유저가 팔로우 안 한 상태라면 팔로우 버튼이 보임

 

 

팔로우 기능 구독 정보 모달 확인)

  • 로그인 후(1번 유저), 프로필 페이지로 이동
    • 구독정보 모달에 팔로우한 유저와 - 언팔로우 버튼 나란히 생성
  • 다른 페이지 이동 (2번 유저)
    • 구독 정보모달에 페이지 유저가 팔로우한 유저 - 언팔로우, 팔로우 버튼 존재
      • 로그인 유저가 이미 팔로우 중이면, 언팔로우 버튼
      • 로그인 유저가 팔로우 하고 있지 않다면, 팔로우 버튼 
      • 하지만 로그인 한 유저는 자기 자신 이므로 팔로우 관련 버튼 없음

 

 

 

🎁 중간 점검
현재까지 구현한 기능들 점검 & 추가하고 싶은 기능* 메모
1. 회원가입
  * 소셜 로그인 
2. 로그인

3. heaer - feed, popular, profile 이동
4. footer - 깃헙, 기술블로그 이동

5. 피드페이지, 로그인 유저가 팔로우한 유저가 등록한 이미지 출력
5. 피드페이지, 스크롤 시 이미지 이어 나오는 페이징 기능

6. 프로필페이지, 이미지 추가 및 랜더링
6. 프로필페이지, 회원정보변경
6. 프로필페이지, 게시물 수와 팔로잉 수 표기
6. 프로필페이지, 페이지 이동하며 팔로우, 언팔로우 가능
6. 프로필페이지, 구독 정보 모달에 팔로우한 유저출력 및  팔로우, 언팔로우 가능

 

 

 

여기까지 팔로우 구현 및 관련 Trouble Shooting부분까지 포스팅 완료하였고

팔로우 기능 구현으로 push 했습니다.

 

 

이어서 좋아요 기능 구현, Popular 페이지 렌더링을 진행하도록 하겠습니다.