Skip to content

Commit

Permalink
[Feature] 팔로잉, 팔로워 목록 조회를 구현한다 (#28)
Browse files Browse the repository at this point in the history
* feature: NoOffset 구현을 위한 VO 클래스, 유틸 추가

* fix: LongIdList, PageArgumentResolver에 예외 상황 추가

* feature: ProfileService 도메인 서비스 작성

* feature: 팔로잉, 팔로워 조회 구현

* feature: 팔로워 삭제 구현
  • Loading branch information
vectorch9 authored Oct 24, 2023
1 parent efb1492 commit 5862555
Show file tree
Hide file tree
Showing 16 changed files with 354 additions and 18 deletions.
2 changes: 2 additions & 0 deletions src/main/java/daybyquest/global/config/WebMvcConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import daybyquest.auth.AuthorizationInterceptor;
import daybyquest.auth.UserIdArgumentResolver;
import daybyquest.global.query.NoOffsetIdPageArgumentResolver;
import java.util.List;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
Expand All @@ -19,5 +20,6 @@ public void addInterceptors(final InterceptorRegistry registry) {
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new UserIdArgumentResolver());
resolvers.add(new NoOffsetIdPageArgumentResolver());
}
}
2 changes: 1 addition & 1 deletion src/main/java/daybyquest/global/error/ExceptionCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ public enum ExceptionCode {
NOT_EXIST_INTEREST("INT-00", BAD_REQUEST, "존재하지 않는 관심사 입니다"),

// Relationship
NOT_FOLLOWING_USER("REL-00", BAD_REQUEST, "팔로우하지 않은 사용자는 취소할 수 없습니다"),
NOT_FOLLOWING_USER("REL-00", BAD_REQUEST, "팔로우하지 않은 사용자입니다"),
NOT_BLOCKING_USER("REL-01", BAD_REQUEST, "차단하지 않은 사용자는 취소할 수 없습니다"),
ALREADY_FOLLOWING_USER("REL-02", BAD_REQUEST, "이미 팔로우한 사용자입니다"),
ALREADY_BLOCKING_USER("REL-03", BAD_REQUEST, "이미 차단한 사용자입니다"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@
import daybyquest.global.error.ExceptionCode;
import java.util.List;

public class BadRequestException extends CustomException{
public class BadRequestException extends CustomException {

public BadRequestException() {
super(ExceptionCode.INVALID_REQUEST);
}

public BadRequestException(ExceptionCode exceptionCode, List<String> fields) {
super(exceptionCode, fields);
Expand Down
21 changes: 21 additions & 0 deletions src/main/java/daybyquest/global/query/LongIdList.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package daybyquest.global.query;

import java.util.List;
import lombok.Getter;

@Getter
public class LongIdList {

private final List<Long> ids;

public LongIdList(final List<Long> ids) {
this.ids = ids;
}

public Long getLastId() {
if (ids.isEmpty()) {
return Long.MAX_VALUE;
}
return ids.get(ids.size() - 1);
}
}
16 changes: 16 additions & 0 deletions src/main/java/daybyquest/global/query/NoOffsetIdPage.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package daybyquest.global.query;

import lombok.Getter;

@Getter
public class NoOffsetIdPage {

private final Long lastId;

private final int limit;

public NoOffsetIdPage(final Long lastId, final int limit) {
this.lastId = lastId;
this.limit = limit;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package daybyquest.global.query;

import daybyquest.global.error.exception.BadRequestException;
import jakarta.annotation.Nonnull;
import java.util.regex.Pattern;
import org.springframework.core.MethodParameter;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;

public class NoOffsetIdPageArgumentResolver implements HandlerMethodArgumentResolver {

private static final int MAX_LIMIT = 15;
private static final Pattern NUMBER = Pattern.compile("^[0-9]*$");


@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.getParameterType().equals(NoOffsetIdPage.class);
}

@Override
public Object resolveArgument(@Nonnull MethodParameter parameter, ModelAndViewContainer mavContainer,
@Nonnull NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
final String lastIdArgument = webRequest.getParameter("lastId");
final String limitArgument = webRequest.getParameter("limit");
return new NoOffsetIdPage(convertAndValidateLastId(lastIdArgument),
convertAndValidateLimit(limitArgument));
}

private Long convertAndValidateLastId(String lastId) {
if (lastId == null) {
return 0L;
}
if (!NUMBER.matcher(lastId).matches()) {
throw new BadRequestException();
}
return Long.parseLong(lastId);
}

private int convertAndValidateLimit(String limit) {
if (limit == null || !NUMBER.matcher(limit).matches()) {
throw new BadRequestException();
}
final int parsedLimit = Integer.parseInt(limit);
if (parsedLimit > MAX_LIMIT) {
throw new BadRequestException();
}
return parsedLimit;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package daybyquest.relation.application;

import daybyquest.relation.domain.Follow;
import daybyquest.relation.domain.Follows;
import daybyquest.user.domain.Users;
import org.springframework.stereotype.Service;

@Service
public class DeleteFollowerService {

private final Follows follows;

private final Users users;

public DeleteFollowerService(final Follows follows, final Users users) {
this.follows = follows;
this.users = users;
}

public void invoke(final Long loginId, final String targetUsername) {
final Long targetId = users.getUserIdByUsername(targetUsername);
final Follow follow = follows.getByUserIdAndTargetId(targetId, loginId);
follows.delete(follow);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package daybyquest.relation.application;

import daybyquest.global.query.LongIdList;
import daybyquest.global.query.NoOffsetIdPage;
import daybyquest.relation.query.FollowDao;
import daybyquest.user.dto.response.PageProfilesResponse;
import daybyquest.user.dto.response.ProfileResponse;
import daybyquest.user.query.ProfileService;
import java.util.List;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class GetFollowersService {

private final FollowDao followDao;

private final ProfileService profileService;

public GetFollowersService(final FollowDao followDao, final ProfileService profileService) {
this.followDao = followDao;
this.profileService = profileService;
}

@Transactional(readOnly = true)
public PageProfilesResponse invoke(final Long loginId, final NoOffsetIdPage page) {
final LongIdList targetIds = followDao.getFollowerIds(loginId, page);
final List<ProfileResponse> profiles = profileService.getProfilesByIdIn(loginId, targetIds.getIds());
return new PageProfilesResponse(profiles, targetIds.getLastId());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package daybyquest.relation.application;

import daybyquest.global.query.LongIdList;
import daybyquest.global.query.NoOffsetIdPage;
import daybyquest.relation.query.FollowDao;
import daybyquest.user.dto.response.PageProfilesResponse;
import daybyquest.user.dto.response.ProfileResponse;
import daybyquest.user.query.ProfileService;
import java.util.List;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class GetFollowingsService {

private final FollowDao followDao;

private final ProfileService profileService;

public GetFollowingsService(final FollowDao followDao, final ProfileService profileService) {
this.followDao = followDao;
this.profileService = profileService;
}

@Transactional(readOnly = true)
public PageProfilesResponse invoke(final Long loginId, final NoOffsetIdPage page) {
final LongIdList targetIds = followDao.getFollowingIds(loginId, page);
final List<ProfileResponse> profiles = profileService.getProfilesByIdIn(loginId, targetIds.getIds());
return new PageProfilesResponse(profiles, targetIds.getLastId());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,16 @@

import daybyquest.auth.Authorization;
import daybyquest.auth.UserId;
import daybyquest.global.query.NoOffsetIdPage;
import daybyquest.relation.application.DeleteFollowService;
import daybyquest.relation.application.DeleteFollowerService;
import daybyquest.relation.application.GetFollowersService;
import daybyquest.relation.application.GetFollowingsService;
import daybyquest.relation.application.SaveFollowService;
import daybyquest.user.dto.response.PageProfilesResponse;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
Expand All @@ -17,10 +23,20 @@ public class FollowController {

private final DeleteFollowService deleteFollowService;

private final DeleteFollowerService deleteFollowerService;

private final GetFollowersService getFollowersService;

private final GetFollowingsService getFollowingsService;

public FollowController(final SaveFollowService saveFollowService,
final DeleteFollowService deleteFollowService) {
final DeleteFollowService deleteFollowService, final DeleteFollowerService deleteFollowerService,
final GetFollowersService getFollowersService, final GetFollowingsService getFollowingsService) {
this.saveFollowService = saveFollowService;
this.deleteFollowService = deleteFollowService;
this.deleteFollowerService = deleteFollowerService;
this.getFollowersService = getFollowersService;
this.getFollowingsService = getFollowingsService;
}

@PostMapping("/profile/{username}/follow")
Expand All @@ -38,4 +54,28 @@ public ResponseEntity<Void> deleteFollow(@UserId final Long loginId,
deleteFollowService.invoke(loginId, username);
return ResponseEntity.ok().build();
}

@DeleteMapping("/profile/{username}/follower")
@Authorization
public ResponseEntity<Void> deleteFollower(@UserId final Long loginId,
@PathVariable final String username) {
deleteFollowerService.invoke(loginId, username);
return ResponseEntity.ok().build();
}

@GetMapping("/followings")
@Authorization
public ResponseEntity<PageProfilesResponse> getFollowings(@UserId final Long loginId,
final NoOffsetIdPage page) {
final PageProfilesResponse response = getFollowingsService.invoke(loginId, page);
return ResponseEntity.ok(response);
}

@GetMapping("/followers")
@Authorization
public ResponseEntity<PageProfilesResponse> getFollowers(@UserId final Long loginId,
final NoOffsetIdPage page) {
final PageProfilesResponse response = getFollowersService.invoke(loginId, page);
return ResponseEntity.ok(response);
}
}
11 changes: 11 additions & 0 deletions src/main/java/daybyquest/relation/query/FollowDao.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package daybyquest.relation.query;

import daybyquest.global.query.LongIdList;
import daybyquest.global.query.NoOffsetIdPage;

public interface FollowDao {

LongIdList getFollowingIds(final Long userId, final NoOffsetIdPage page);

LongIdList getFollowerIds(final Long targetId, final NoOffsetIdPage page);
}
36 changes: 36 additions & 0 deletions src/main/java/daybyquest/relation/query/FollowDaoQuerydslImpl.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package daybyquest.relation.query;

import static daybyquest.relation.domain.QFollow.follow;

import com.querydsl.jpa.impl.JPAQueryFactory;
import daybyquest.global.query.LongIdList;
import daybyquest.global.query.NoOffsetIdPage;
import org.springframework.stereotype.Repository;

@Repository
public class FollowDaoQuerydslImpl implements FollowDao {

private final JPAQueryFactory factory;

public FollowDaoQuerydslImpl(final JPAQueryFactory factory) {
this.factory = factory;
}

@Override
public LongIdList getFollowingIds(final Long userId, final NoOffsetIdPage page) {
return new LongIdList(factory.select(follow.targetId)
.from(follow)
.where(follow.userId.eq(userId).and(follow.targetId.gt(page.getLastId())))
.limit(page.getLimit())
.fetch());
}

@Override
public LongIdList getFollowerIds(final Long targetId, final NoOffsetIdPage page) {
return new LongIdList(factory.select(follow.userId)
.from(follow)
.where(follow.targetId.eq(targetId).and(follow.userId.gt(page.getLastId())))
.limit(page.getLimit())
.fetch());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package daybyquest.user.dto.response;

import java.util.List;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
public class PageProfilesResponse {

private List<ProfileResponse> users;

private Long nextId;

public PageProfilesResponse(final List<ProfileResponse> users, final Long nextId) {
this.users = users;
this.nextId = nextId;
}
}
4 changes: 4 additions & 0 deletions src/main/java/daybyquest/user/query/ProfileDao.java
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
package daybyquest.user.query;

import daybyquest.user.domain.Profile;
import java.util.Collection;
import java.util.List;

public interface ProfileDao {

Profile getByUsername(final Long userId, final String username);

Profile getMine(final Long userId);

List<Profile> findAllByUserIdIn(final Long userId, final Collection<Long> targetIds);
}
Loading

0 comments on commit 5862555

Please sign in to comment.