-
Notifications
You must be signed in to change notification settings - Fork 1
[feat] 팔로잉 상태 변경 api 개발 #76
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
a26e358
a33c8e3
0594ade
cd4742c
bf2fda7
12f1378
13b2738
8433d9e
2d0ad52
52d4917
155a795
4df0745
c799eb7
57edf6b
00c49ea
51595e0
c604db0
db279de
b6dcd4b
799145e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| package konkuk.thip.user.adapter.in.web.request; | ||
|
|
||
| import jakarta.validation.constraints.NotNull; | ||
| import konkuk.thip.user.application.port.in.dto.UserFollowCommand; | ||
|
|
||
| public record UserFollowRequest( | ||
| @NotNull(message = "type은 필수 파라미터입니다.") | ||
| Boolean type // true -> 팔로우, false -> 언팔로우 | ||
| ) { | ||
| public static UserFollowCommand toCommand(Long userId, Long targetUserId, Boolean type) { | ||
| return new UserFollowCommand(userId, targetUserId, type); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| package konkuk.thip.user.adapter.in.web.response; | ||
|
|
||
| public record UserFollowResponse( | ||
| boolean isFollowing | ||
| ) { | ||
| public static UserFollowResponse of(boolean isFollowing) { | ||
| return new UserFollowResponse(isFollowing); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| package konkuk.thip.user.adapter.out.persistence; | ||
|
|
||
| import konkuk.thip.common.exception.EntityNotFoundException; | ||
| import konkuk.thip.user.adapter.out.jpa.FollowingJpaEntity; | ||
| import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; | ||
| import konkuk.thip.user.adapter.out.mapper.FollowingMapper; | ||
| import konkuk.thip.user.application.port.out.FollowingCommandPort; | ||
| import konkuk.thip.user.domain.Following; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.stereotype.Repository; | ||
|
|
||
| import java.util.Optional; | ||
|
|
||
| import static konkuk.thip.common.exception.code.ErrorCode.FOLLOW_NOT_FOUND; | ||
| import static konkuk.thip.common.exception.code.ErrorCode.USER_NOT_FOUND; | ||
|
|
||
| @Repository | ||
| @RequiredArgsConstructor | ||
| public class FollowingCommandPersistenceAdapter implements FollowingCommandPort { | ||
|
|
||
| private final FollowingJpaRepository followingJpaRepository; | ||
| private final UserJpaRepository userJpaRepository; | ||
|
|
||
| private final FollowingMapper followingMapper; | ||
|
|
||
| @Override | ||
| public Optional<Following> findByUserIdAndTargetUserId(Long userId, Long targetUserId) { | ||
| Optional<FollowingJpaEntity> followingJpaEntity = followingJpaRepository.findByUserAndTargetUser(userId, targetUserId); | ||
| return followingJpaEntity.map(followingMapper::toDomainEntity); | ||
| } | ||
|
|
||
| @Override | ||
| public void save(Following following) { // insert용 | ||
| UserJpaEntity userJpaEntity = userJpaRepository.findById(following.getUserId()).orElseThrow( | ||
| () -> new EntityNotFoundException(USER_NOT_FOUND)); | ||
|
|
||
| UserJpaEntity targetUser = userJpaRepository.findById(following.getFollowingUserId()).orElseThrow( | ||
| () -> new EntityNotFoundException(USER_NOT_FOUND)); | ||
|
|
||
| followingJpaRepository.save(followingMapper.toJpaEntity(userJpaEntity, targetUser)); | ||
| } | ||
|
|
||
| @Override | ||
| public void updateStatus(Following following) { // 상태변경 용 | ||
| FollowingJpaEntity entity = followingJpaRepository.findByUserAndTargetUser(following.getUserId(), following.getFollowingUserId()) | ||
| .orElseThrow(() -> new EntityNotFoundException(FOLLOW_NOT_FOUND)); | ||
|
|
||
| entity.setStatus(following.getStatus()); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,12 @@ | ||
| package konkuk.thip.user.adapter.out.persistence; | ||
|
|
||
| import konkuk.thip.user.adapter.out.jpa.FollowingJpaEntity; | ||
|
|
||
| import java.util.Optional; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
|
|
||
| public interface FollowingQueryRepository { | ||
| Map<Long, Integer> countByFollowingUserIds(List<Long> userIds); | ||
| Optional<FollowingJpaEntity> findByUserAndTargetUser(Long userId, Long targetUserId); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chainCQRS 분리 원칙에 대한 검토가 필요합니다. 메서드명과 Optional 반환 타입 사용은 적절하지만, CQRS 분리 관점에서 확인이 필요합니다. 학습된 컨벤션에 따르면 "CommandPort에는 findByXXX를 통해 도메인 엔티티를 찾아오는 메서드를 추가"해야 하는데, 이 메서드는 QueryRepository에 위치하고 있습니다. 이 메서드가 실제로 command 연산(팔로우/언팔로우 상태 변경)에 사용된다면 CommandPort로 이동하는 것이 CQRS 원칙에 더 부합할 수 있습니다. 다음 스크립트로 이 메서드의 사용 패턴을 확인해보세요: 🏁 Script executed: #!/bin/bash
# 이 메서드가 command 연산에 사용되는지 확인
rg -A 5 -B 5 "findByUserAndTargetUser" --type javaLength of output: 7749 🏁 Script executed: #!/bin/bash
# Display the FollowingJpaRepository to see its declarations
rg -A 5 -B 5 "interface FollowingJpaRepository" -n src/main/java/konkuk/thip/user/adapter/out/persistenceLength of output: 1179 🏁 Script executed: #!/bin/bash
# 포트 인터페이스 확인
rg -A 5 -B 5 "interface FollowingCommandPort" -n src/main/java/konkuk/thip
rg -A 5 -B 5 "interface FollowingQueryPort" -n src/main/java/konkuk/thipLength of output: 2175 CQRS 포트 분리: Command용 현재
위 변경으로 🤖 Prompt for AI Agents |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| package konkuk.thip.user.application.port.in; | ||
|
|
||
|
|
||
| import konkuk.thip.user.application.port.in.dto.UserFollowCommand; | ||
|
|
||
| public interface UserFollowUsecase { | ||
| Boolean changeFollowingState(UserFollowCommand followCommand); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| package konkuk.thip.user.application.port.in.dto; | ||
|
|
||
| public record UserFollowCommand( | ||
| Long userId, | ||
| Long targetUserId, | ||
| Boolean type // true -> 팔로우, false -> 언팔로우 | ||
| ) { | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| package konkuk.thip.user.application.port.out; | ||
|
|
||
| import konkuk.thip.user.domain.Following; | ||
|
|
||
| import java.util.Optional; | ||
|
|
||
| public interface FollowingCommandPort { | ||
|
|
||
| Optional<Following> findByUserIdAndTargetUserId(Long userId, Long targetUserId); | ||
|
|
||
| void save(Following following); | ||
|
|
||
| void updateStatus(Following following); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| package konkuk.thip.user.application.service; | ||
|
|
||
| import konkuk.thip.common.exception.InvalidStateException; | ||
| import konkuk.thip.user.application.port.in.UserFollowUsecase; | ||
| import konkuk.thip.user.application.port.in.dto.UserFollowCommand; | ||
| import konkuk.thip.user.application.port.out.FollowingCommandPort; | ||
| import konkuk.thip.user.domain.Following; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.stereotype.Service; | ||
| import org.springframework.transaction.annotation.Transactional; | ||
|
|
||
| import java.util.Objects; | ||
| import java.util.Optional; | ||
|
|
||
| import static konkuk.thip.common.exception.code.ErrorCode.USER_ALREADY_UNFOLLOWED; | ||
| import static konkuk.thip.common.exception.code.ErrorCode.USER_CANNOT_FOLLOW_SELF; | ||
|
|
||
| @Service | ||
| @RequiredArgsConstructor | ||
| public class UserFollowService implements UserFollowUsecase { | ||
|
|
||
| private final FollowingCommandPort followingCommandPort; | ||
|
|
||
| @Override | ||
| @Transactional | ||
| public Boolean changeFollowingState(UserFollowCommand followCommand) { | ||
| Long userId = followCommand.userId(); | ||
| Long targetUserId = followCommand.targetUserId(); | ||
| Boolean type = followCommand.type(); | ||
|
|
||
| validateParams(userId, targetUserId); | ||
|
|
||
| Optional<Following> optionalFollowing = followingCommandPort.findByUserIdAndTargetUserId(userId, targetUserId); | ||
|
|
||
| if (optionalFollowing.isPresent()) { // 이미 팔로우 관계가 존재하는 경우 | ||
| Following following = optionalFollowing.get(); | ||
| boolean isFollowing = following.changeFollowingState(type); | ||
| followingCommandPort.updateStatus(following); | ||
| return isFollowing; | ||
| } else { // 팔로우 관계가 존재하지 않는 경우 | ||
| if (!type) { | ||
| throw new InvalidStateException(USER_ALREADY_UNFOLLOWED); // 언팔로우 요청인데 팔로우 관계가 존재하지 않으므로 이미 언팔로우 상태 | ||
| } | ||
| followingCommandPort.save(Following.withoutId(userId, targetUserId)); | ||
| return true; // 새로 팔로우한 경우 | ||
| } | ||
| } | ||
|
|
||
| private void validateParams(Long userId, Long targetUserId) { | ||
| if(Objects.equals(userId, targetUserId)) { | ||
| throw new InvalidStateException(USER_CANNOT_FOLLOW_SELF); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍🏻