Skip to content

Commit c3327fb

Browse files
authored
[FEAT] 팔로워 목록 조회 API 개발 (#204)
* ✨feat:팔로워 목록 조회 API 개발 - http 테스트 작성 * 🐛fix: 팔로워 조회 시 맞팔 여부 필드 추가
1 parent 37bce00 commit c3327fb

File tree

8 files changed

+178
-52
lines changed

8 files changed

+178
-52
lines changed

src/main/java/team/wego/wegobackend/user/application/FollowService.java

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
package team.wego.wegobackend.user.application;
22

3+
import java.util.ArrayList;
4+
import java.util.HashSet;
35
import java.util.List;
6+
import java.util.Set;
47
import lombok.RequiredArgsConstructor;
58
import lombok.extern.slf4j.Slf4j;
69
import org.springframework.context.ApplicationEventPublisher;
710
import org.springframework.stereotype.Service;
811
import org.springframework.transaction.annotation.Transactional;
9-
import team.wego.wegobackend.notification.application.SseEmitterService;
10-
import team.wego.wegobackend.notification.application.dto.NotificationType;
11-
import team.wego.wegobackend.notification.application.dto.response.NotificationResponse;
12-
import team.wego.wegobackend.notification.domain.Notification;
13-
import team.wego.wegobackend.notification.repository.NotificationRepository;
1412
import team.wego.wegobackend.user.application.dto.response.FollowListResponse;
1513
import team.wego.wegobackend.user.application.dto.response.FollowResponse;
14+
import team.wego.wegobackend.user.application.dto.response.FollowerListResponse;
15+
import team.wego.wegobackend.user.application.dto.response.WrapperFollowerResponse;
1616
import team.wego.wegobackend.user.application.event.FollowEvent;
1717
import team.wego.wegobackend.user.domain.Follow;
1818
import team.wego.wegobackend.user.domain.User;
@@ -34,10 +34,6 @@ public class FollowService {
3434

3535
private final UserRepository userRepository;
3636

37-
private final NotificationRepository notificationRepository;
38-
39-
private final SseEmitterService sseEmitterService;
40-
4137
private final ApplicationEventPublisher eventPublisher;
4238

4339
public void follow(String followNickname, Long followerId) {
@@ -101,4 +97,30 @@ public FollowListResponse followList(Long userId, Long cursor, Integer size) {
10197

10298
return new FollowListResponse(list, nextCursor);
10399
}
100+
101+
@Transactional(readOnly = true)
102+
public FollowerListResponse followerList(Long userId, Long cursor, Integer size) {
103+
104+
if (!userRepository.existsById(userId)) {
105+
throw new UserNotFoundException();
106+
}
107+
108+
List<FollowResponse> list = followRepository.findFollowerList(userId, cursor, size);
109+
110+
// 맞팔로우 여부 조회: 내가 팔로우한 사람들의 ID 조회
111+
Set<Long> followingUserIds = followRepository.findFolloweeIdsByFollowerId(userId);
112+
113+
List<WrapperFollowerResponse> result = new ArrayList<>();
114+
115+
for(FollowResponse follower : list) {
116+
boolean isFollow = followingUserIds.contains(follower.getUserId());
117+
follower.getUserId(), follower.getNickname(), isFollow);
118+
WrapperFollowerResponse response = new WrapperFollowerResponse(follower, isFollow);
119+
result.add(response);
120+
}
121+
122+
Long nextCursor = list.isEmpty() ? null : list.getLast().getFollowId();
123+
124+
return new FollowerListResponse(result, nextCursor);
125+
}
104126
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package team.wego.wegobackend.user.application.dto.response;
2+
3+
import java.util.List;
4+
5+
public record FollowerListResponse(List<WrapperFollowerResponse> items, Long nextCursor) {
6+
7+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package team.wego.wegobackend.user.application.dto.response;
2+
3+
import lombok.Getter;
4+
5+
@Getter
6+
public class WrapperFollowerResponse {
7+
8+
private final Long followId; //cursor
9+
private final Long userId;
10+
private final String profileImage;
11+
private final String nickname;
12+
private final String profileMessage;
13+
private final boolean isFollow;
14+
15+
public WrapperFollowerResponse(FollowResponse followResponse, boolean isFollow) {
16+
this.followId = followResponse.getFollowId();
17+
this.userId = followResponse.getUserId();
18+
this.profileImage = followResponse.getProfileImage();
19+
this.nickname = followResponse.getNickname();
20+
this.profileMessage = followResponse.getProfileMessage();
21+
this.isFollow = isFollow;
22+
}
23+
24+
}

src/main/java/team/wego/wegobackend/user/presentation/UserController.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import team.wego.wegobackend.user.application.dto.request.ProfileUpdateRequest;
3030
import team.wego.wegobackend.user.application.dto.response.AvailabilityResponse;
3131
import team.wego.wegobackend.user.application.dto.response.FollowListResponse;
32+
import team.wego.wegobackend.user.application.dto.response.FollowerListResponse;
3233
import team.wego.wegobackend.user.application.dto.response.UserInfoResponse;
3334

3435
@Slf4j
@@ -174,6 +175,23 @@ public ResponseEntity<ApiResponse<FollowListResponse>> followList(
174175
.body(ApiResponse.success(200, response));
175176
}
176177

178+
/**
179+
* 팔로워 리스트 조회
180+
*/
181+
@GetMapping("/{userId}/follower")
182+
public ResponseEntity<ApiResponse<FollowerListResponse>> followerList(
183+
@PathVariable Long userId, //다른 유저 조회를 위한 파라메터
184+
@RequestParam(required = false) Long cursor,
185+
@RequestParam(defaultValue = "20") @Min(1) @Max(100) Integer size
186+
) {
187+
188+
FollowerListResponse response = followService.followerList(userId, cursor, size);
189+
190+
return ResponseEntity
191+
.status(HttpStatus.OK)
192+
.body(ApiResponse.success(200, response));
193+
}
194+
177195
/**
178196
* 이메일 중복검사
179197
*/

src/main/java/team/wego/wegobackend/user/repository/FollowRepository.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
package team.wego.wegobackend.user.repository;
22

33
import java.util.Optional;
4+
import java.util.Set;
45
import org.springframework.data.jpa.repository.JpaRepository;
6+
import org.springframework.data.jpa.repository.Query;
7+
import org.springframework.data.repository.query.Param;
58
import team.wego.wegobackend.user.domain.Follow;
69

710
public interface FollowRepository extends JpaRepository<Follow, Long>, FollowRepositoryCustom {
@@ -12,4 +15,7 @@ public interface FollowRepository extends JpaRepository<Follow, Long>, FollowRep
1215
void deleteByFollowerId(Long userId);
1316

1417
void deleteByFolloweeId(Long userId);
18+
19+
@Query("SELECT f.followee.id FROM Follow f WHERE f.follower.id = :followerId")
20+
Set<Long> findFolloweeIdsByFollowerId(@Param("followerId") Long followerId);
1521
}

src/main/java/team/wego/wegobackend/user/repository/FollowRepositoryCustom.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@ List<FollowResponse> findFollowingList(
1313
int size
1414
);
1515

16+
List<FollowResponse> findFollowerList(
17+
Long followerId,
18+
Long cursorFollowId,
19+
int size
20+
);
21+
1622
List<FollowerNotifyRow> findFollowersForNotify(Long followeeId, Long cursorFollowId, int size);
1723

1824
}

src/main/java/team/wego/wegobackend/user/repository/FollowRepositoryImpl.java

Lines changed: 71 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -19,66 +19,95 @@ public class FollowRepositoryImpl implements FollowRepositoryCustom {
1919

2020
@Override
2121
public List<FollowResponse> findFollowingList(
22-
Long followerId,
23-
Long cursorFollowId,
24-
int size
22+
Long followerId,
23+
Long cursorFollowId,
24+
int size
2525
) {
2626

2727
QFollow follow = QFollow.follow;
2828
QUser user = QUser.user;
2929

3030
return jpaQueryFactory
31-
.select(new QFollowResponse(
32-
follow.id,
33-
user.id,
34-
user.profileImage,
35-
user.nickName,
36-
user.profileMessage
37-
))
38-
.from(follow)
39-
.join(follow.followee, user)
40-
.where(
41-
follow.follower.id.eq(followerId),
42-
itCursor(cursorFollowId)
43-
)
44-
.orderBy(follow.id.desc()) //최신 팔로우 순
45-
.limit(size)
46-
.fetch();
31+
.select(new QFollowResponse(
32+
follow.id,
33+
user.id,
34+
user.profileImage,
35+
user.nickName,
36+
user.profileMessage
37+
))
38+
.from(follow)
39+
.join(follow.followee, user)
40+
.where(
41+
follow.follower.id.eq(followerId),
42+
itCursor(cursorFollowId)
43+
)
44+
.orderBy(follow.id.desc()) //최신 팔로우 순
45+
.limit(size)
46+
.fetch();
47+
}
48+
49+
@Override
50+
public List<FollowResponse> findFollowerList(
51+
Long followId,
52+
Long cursorFollowId,
53+
int size
54+
) {
55+
56+
QFollow follow = QFollow.follow;
57+
QUser user = QUser.user;
58+
59+
return jpaQueryFactory
60+
.select(new QFollowResponse(
61+
follow.id,
62+
user.id,
63+
user.profileImage,
64+
user.nickName,
65+
user.profileMessage
66+
))
67+
.from(follow)
68+
.join(follow.follower, user)
69+
.where(
70+
follow.followee.id.eq(followId),
71+
itCursor(cursorFollowId)
72+
)
73+
.orderBy(follow.id.desc()) //최신 팔로우 순
74+
.limit(size)
75+
.fetch();
4776
}
4877

4978
private BooleanExpression itCursor(Long cursorFollowId) {
5079
return cursorFollowId == null
51-
? null
52-
: QFollow.follow.id.lt(cursorFollowId);
80+
? null
81+
: QFollow.follow.id.lt(cursorFollowId);
5382
}
5483

5584
@Override
5685
public List<FollowerNotifyRow> findFollowersForNotify(
57-
Long followeeId,
58-
Long cursorFollowId,
59-
int size
86+
Long followeeId,
87+
Long cursorFollowId,
88+
int size
6089
) {
6190
QFollow follow = QFollow.follow;
6291
QUser user = QUser.user;
6392

6493
return jpaQueryFactory
65-
.select(Projections.constructor(
66-
FollowerNotifyRow.class,
67-
follow.id,
68-
user.id,
69-
user.nickName,
70-
user.profileImage
71-
))
72-
.from(follow)
73-
.join(follow.follower, user)
74-
.where(
75-
follow.followee.id.eq(followeeId),
76-
cursorFollowId == null ? null : follow.id.lt(cursorFollowId),
77-
user.notificationEnabled.isTrue(),
78-
user.deleted.isFalse()
79-
)
80-
.orderBy(follow.id.desc())
81-
.limit(size)
82-
.fetch();
94+
.select(Projections.constructor(
95+
FollowerNotifyRow.class,
96+
follow.id,
97+
user.id,
98+
user.nickName,
99+
user.profileImage
100+
))
101+
.from(follow)
102+
.join(follow.follower, user)
103+
.where(
104+
follow.followee.id.eq(followeeId),
105+
cursorFollowId == null ? null : follow.id.lt(cursorFollowId),
106+
user.notificationEnabled.isTrue(),
107+
user.deleted.isFalse()
108+
)
109+
.orderBy(follow.id.desc())
110+
.limit(size)
111+
.fetch();
83112
}
84113
}

src/test/http/user/user-api.http

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ Content-Type: application/json
6868
}
6969

7070
### 팔로우 요청
71-
POST http://localhost:8080/api/v1/users/follow?followNickname=user0
71+
POST http://localhost:8080/api/v1/users/follow?followNickname=ttest
7272
Authorization: Bearer {{accessToken}}
7373

7474
### 팔로우 취소
@@ -91,6 +91,20 @@ GET http://localhost:8080/api/v1/users/1/follow?size=10
9191
### 팔로우 리스트 조회 (cursor)
9292
GET http://localhost:8080/api/v1/users/1/follow?cursor={{nextCursor}}&size=10
9393

94+
> {%
95+
client.global.set("nextCursor", response.body.data.nextCursor);
96+
%}
97+
98+
### 팔로워 리스트 조회 (초기값)
99+
GET http://localhost:8080/api/v1/users/102/follower?size=10
100+
101+
> {%
102+
client.global.set("nextCursor", response.body.data.nextCursor);
103+
%}
104+
105+
### 팔로워 리스트 조회 (cursor)
106+
GET http://localhost:8080/api/v1/users/1/follower?cursor={{nextCursor}}&size=10
107+
94108
> {%
95109
client.global.set("nextCursor", response.body.data.nextCursor);
96110
%}

0 commit comments

Comments
 (0)