diff --git a/.gitignore b/.gitignore index 1284d26..63e193a 100644 --- a/.gitignore +++ b/.gitignore @@ -223,4 +223,7 @@ gradle-app.setting # Java heap dump *.hprof +#spring log datas +logs +logs/* # End of https://www.toptal.com/developers/gitignore/api/windows,macos,intellij+all,visualstudiocode,java,gradle \ No newline at end of file diff --git a/src/main/java/com/sequence/anonymous/friend/application/FriendService.java b/src/main/java/com/sequence/anonymous/friend/application/FriendService.java new file mode 100644 index 0000000..a1f9c1f --- /dev/null +++ b/src/main/java/com/sequence/anonymous/friend/application/FriendService.java @@ -0,0 +1,107 @@ +package com.sequence.anonymous.friend.application; + +import com.google.common.base.Preconditions; +import com.sequence.anonymous.friend.domain.Friend; +import com.sequence.anonymous.friend.domain.repository.FriendRepository; +import com.sequence.anonymous.friend.presentation.dto.FriendResponse; +import com.sequence.anonymous.invite.domain.Invite; +import com.sequence.anonymous.invite.domain.Kind; +import com.sequence.anonymous.invite.domain.Status; +import com.sequence.anonymous.invite.domain.repository.InviteRepository; +import com.sequence.anonymous.invite.presentation.dto.InviteResponse; +import com.sequence.anonymous.user.domain.repository.UserRepository; +import com.sequence.anonymous.user.domain.user.User; +import java.util.List; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.webjars.NotFoundException; + +@Service +@RequiredArgsConstructor +public class FriendService { + + private final FriendRepository friendRepository; + + private final UserRepository userRepository; + + private final InviteRepository inviteRepository; + + @Transactional(readOnly = true) + public List findFriendsByUserId(Long userId) { + List friendList = friendRepository.findByUserIdWithUserAndFriend(userId); + + return friendList.stream() + .map(FriendResponse::fromFriend) + .toList(); + } + + @Transactional(readOnly = true) + public List findRequestsByInviterId(Long inviterId) { + List inviteList = inviteRepository.findByInviterIdAndStatusWithInviterAndInvitee(inviterId, Status.WAIT); + + return inviteList.stream() + .map(InviteResponse::fromInvite) + .toList(); + } + + @Transactional(readOnly = true) + public List findRequestsByInviteeId(Long inviteeId) { + List inviteList = inviteRepository.findByInviterIdAndStatusWithInviterAndInvitee(inviteeId, Status.WAIT); + + return inviteList.stream() + .map(InviteResponse::fromInvite) + .toList(); + } + + @Transactional + public void createNewRequest(Long inviterId, Long inviteeId) { + + Preconditions.checkArgument(inviterId != inviteeId, "inviterId and inviteeId must be different"); + Optional optionalInvite = inviteRepository.findByInviterIdAndInviteeIdAndStatus( + inviterId, + inviteeId, Status.WAIT); + optionalInvite.ifPresent(invite -> { + throw new RuntimeException("duplicate request"); + }); + + Optional optionalFriend = friendRepository.findByUserIdAndFriendId(inviterId, + inviteeId); + optionalFriend.ifPresent(friend -> { + throw new RuntimeException("already a friend"); + }); + + User inviter = userRepository.findById(inviterId) + .orElseThrow(() -> new NotFoundException("inviter not found")); + User invitee = userRepository.findById(inviteeId) + .orElseThrow(() -> new NotFoundException("invitee not found")); + + inviteRepository.save(new Invite(inviter, invitee, Kind.FRIEND)); + } + + @Transactional + public void acceptRequest(Long id) { + Invite invite = inviteRepository.findById(id) + .orElseThrow(() -> new NotFoundException("invite request not found")); + Preconditions.checkArgument(invite.getStatus() != Status.DONE, "request has already been processed"); + + friendRepository.save(new Friend(invite.getInviter(), invite.getInvitee())); + friendRepository.save(new Friend(invite.getInvitee(), invite.getInviter())); + invite.markAsDone(); + } + + @Transactional + public void dismissRequest(Long id) { + Invite invite = inviteRepository.findById(id) + .orElseThrow(() -> new NotFoundException("invite request not found")); + Preconditions.checkArgument(invite.getStatus() != Status.DONE, "request has already been processed"); + + invite.markAsDone(); + } + + @Transactional + public void deleteById(Long id) { + friendRepository.deleteById(id); + } +} diff --git a/src/main/java/com/sequence/anonymous/friend/domain/Friend.java b/src/main/java/com/sequence/anonymous/friend/domain/Friend.java new file mode 100644 index 0000000..7a67909 --- /dev/null +++ b/src/main/java/com/sequence/anonymous/friend/domain/Friend.java @@ -0,0 +1,43 @@ +package com.sequence.anonymous.friend.domain; + +import com.google.common.base.Preconditions; +import com.sequence.anonymous.user.domain.user.User; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Friend { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "friend_id") + private User friend; + + public Friend(User user, User friend) { + Preconditions.checkArgument(user != null, "user must be provided"); + Preconditions.checkArgument(friend != null, "friend must be provided"); + + this.user = user; + this.friend = friend; + } +} diff --git a/src/main/java/com/sequence/anonymous/friend/domain/repository/FriendRepository.java b/src/main/java/com/sequence/anonymous/friend/domain/repository/FriendRepository.java new file mode 100644 index 0000000..71918ef --- /dev/null +++ b/src/main/java/com/sequence/anonymous/friend/domain/repository/FriendRepository.java @@ -0,0 +1,17 @@ +package com.sequence.anonymous.friend.domain.repository; + +import com.sequence.anonymous.friend.domain.Friend; +import java.util.List; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +@Repository +public interface FriendRepository extends JpaRepository { + + @Query("SELECT f from Friend AS f JOIN FETCH f.friend JOIN FETCH f.user WHERE f.user.id= :userId") + List findByUserIdWithUserAndFriend(Long userId); + + Optional findByUserIdAndFriendId(Long userId, Long friendId); +} diff --git a/src/main/java/com/sequence/anonymous/friend/presentation/FriendController.java b/src/main/java/com/sequence/anonymous/friend/presentation/FriendController.java new file mode 100644 index 0000000..a94a0ea --- /dev/null +++ b/src/main/java/com/sequence/anonymous/friend/presentation/FriendController.java @@ -0,0 +1,77 @@ +package com.sequence.anonymous.friend.presentation; + +import com.sequence.anonymous.friend.application.FriendService; +import com.sequence.anonymous.friend.domain.Friend; +import com.sequence.anonymous.friend.presentation.dto.FriendResponse; +import com.sequence.anonymous.invite.domain.Invite; +import com.sequence.anonymous.invite.presentation.dto.InviteResponse; +import com.sequence.anonymous.security.CustomOAuth2User; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +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.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/users/friends") +public class FriendController { + + private final FriendService friendService; + + @GetMapping("") + public ResponseEntity> findAllFriends( + @AuthenticationPrincipal CustomOAuth2User user) { + Long userId = user.getId(); + + return ResponseEntity.ok(friendService.findFriendsByUserId(userId)); + } + + @DeleteMapping("/{id}") + public ResponseEntity deleteFriend(@PathVariable Long id) { + friendService.deleteById(id); + return ResponseEntity.ok().build(); + } + + @PostMapping("/invites/{id}/accept") + public ResponseEntity acceptRequest(@PathVariable Long id) { + friendService.acceptRequest(id); + return ResponseEntity.ok().build(); + } + + @PostMapping("/invites/{id}/dismiss") + public ResponseEntity dismissRequest(@PathVariable Long id) { + friendService.dismissRequest(id); + return ResponseEntity.ok().build(); + } + + @PostMapping("/{id}/invitation") + public ResponseEntity createNewRequest(@PathVariable Long id, + @AuthenticationPrincipal CustomOAuth2User user) { + Long userId = user.getId(); + + friendService.createNewRequest(userId, id); + return ResponseEntity.ok().build(); + } + + @GetMapping("/invites") + public ResponseEntity> findAllRequests( + @AuthenticationPrincipal CustomOAuth2User user, @RequestParam RequestType type) { + Long userId = user.getId(); + List inviteResponseList; + + switch (type) { + case SENT -> inviteResponseList = friendService.findRequestsByInviterId(userId); + case RECEIVED -> inviteResponseList = friendService.findRequestsByInviteeId(userId); + default -> throw new IllegalArgumentException("invalid request type: " + type.toString()); + } + + return ResponseEntity.ok(inviteResponseList); + } +} diff --git a/src/main/java/com/sequence/anonymous/friend/presentation/RequestType.java b/src/main/java/com/sequence/anonymous/friend/presentation/RequestType.java new file mode 100644 index 0000000..fc51f3d --- /dev/null +++ b/src/main/java/com/sequence/anonymous/friend/presentation/RequestType.java @@ -0,0 +1,5 @@ +package com.sequence.anonymous.friend.presentation; + +public enum RequestType { + SENT, RECEIVED +} diff --git a/src/main/java/com/sequence/anonymous/friend/presentation/dto/FriendResponse.java b/src/main/java/com/sequence/anonymous/friend/presentation/dto/FriendResponse.java new file mode 100644 index 0000000..396b31a --- /dev/null +++ b/src/main/java/com/sequence/anonymous/friend/presentation/dto/FriendResponse.java @@ -0,0 +1,18 @@ +package com.sequence.anonymous.friend.presentation.dto; + +import com.google.common.base.Preconditions; +import com.sequence.anonymous.friend.domain.Friend; +import com.sequence.anonymous.user.domain.user.User; + +public record FriendResponse (Long id, User friend) { + + public FriendResponse { + Preconditions.checkArgument(friend != null, "friend must be provided"); + } + + public static FriendResponse fromFriend(Friend friend) { + Preconditions.checkArgument(friend != null, "friend must be provided"); + + return new FriendResponse(friend.getId(), friend.getFriend()); + } +} diff --git a/src/main/java/com/sequence/anonymous/invite/domain/Invite.java b/src/main/java/com/sequence/anonymous/invite/domain/Invite.java new file mode 100644 index 0000000..21043d2 --- /dev/null +++ b/src/main/java/com/sequence/anonymous/invite/domain/Invite.java @@ -0,0 +1,64 @@ +package com.sequence.anonymous.invite.domain; + +import com.google.common.base.Preconditions; +import com.sequence.anonymous.user.domain.user.User; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Invite { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "inviter_id") + private User inviter; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "invitee_id") + private User invitee; + + @Enumerated(EnumType.STRING) + @Column(length = 15) + private Kind kind; + + @Enumerated(EnumType.STRING) + @Column(length = 10) + private Status status; + + public Invite(User inviter, User invitee, Kind kind) { + this(inviter, invitee, kind, Status.WAIT); + } + + private Invite(User inviter, User invitee, Kind kind, Status status) { + Preconditions.checkArgument(inviter != null, "inviter must be provided"); + Preconditions.checkArgument(invitee != null, "invitee must be provided"); + Preconditions.checkArgument(kind != null, "kind must be provided"); + Preconditions.checkArgument(status != null, "status must be provided"); + Preconditions.checkArgument(status != Status.DONE, "initial value of status cannot be DONE"); + + this.inviter = inviter; + this.invitee = invitee; + this.kind = kind; + this.status = status; + } + + public void markAsDone() { + this.status = Status.DONE; + } +} diff --git a/src/main/java/com/sequence/anonymous/invite/domain/Kind.java b/src/main/java/com/sequence/anonymous/invite/domain/Kind.java new file mode 100644 index 0000000..fce7fd3 --- /dev/null +++ b/src/main/java/com/sequence/anonymous/invite/domain/Kind.java @@ -0,0 +1,5 @@ +package com.sequence.anonymous.invite.domain; + +public enum Kind { + MATCH_POST, FRIEND +} diff --git a/src/main/java/com/sequence/anonymous/invite/domain/Status.java b/src/main/java/com/sequence/anonymous/invite/domain/Status.java new file mode 100644 index 0000000..91c2eab --- /dev/null +++ b/src/main/java/com/sequence/anonymous/invite/domain/Status.java @@ -0,0 +1,5 @@ +package com.sequence.anonymous.invite.domain; + +public enum Status { + WAIT, DONE +} diff --git a/src/main/java/com/sequence/anonymous/invite/domain/repository/InviteRepository.java b/src/main/java/com/sequence/anonymous/invite/domain/repository/InviteRepository.java new file mode 100644 index 0000000..6d4fb4d --- /dev/null +++ b/src/main/java/com/sequence/anonymous/invite/domain/repository/InviteRepository.java @@ -0,0 +1,22 @@ +package com.sequence.anonymous.invite.domain.repository; + +import com.sequence.anonymous.invite.domain.Invite; +import com.sequence.anonymous.invite.domain.Status; +import java.util.List; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +@Repository +public interface InviteRepository extends JpaRepository { + + @Query("SELECT i FROM Invite AS i JOIN FETCH i.invitee JOIN FETCH i.inviter WHERE i.inviter.id = :inviterId AND i.status = :status") + List findByInviterIdAndStatusWithInviterAndInvitee(Long inviterId, Status status); + + @Query("SELECT i FROM Invite AS i JOIN FETCH i.invitee JOIN FETCH i.inviter WHERE i.invitee.id = :inviteeId AND i.status = :status") + List findByInviteeIdAndStatusWithInviterAndInvitee(Long inviteeId, Status status); + + Optional findByInviterIdAndInviteeIdAndStatus(Long inviterId, Long inviteeId, Status status); + +} diff --git a/src/main/java/com/sequence/anonymous/invite/presentation/dto/InviteResponse.java b/src/main/java/com/sequence/anonymous/invite/presentation/dto/InviteResponse.java new file mode 100644 index 0000000..ea5422d --- /dev/null +++ b/src/main/java/com/sequence/anonymous/invite/presentation/dto/InviteResponse.java @@ -0,0 +1,25 @@ +package com.sequence.anonymous.invite.presentation.dto; + +import com.google.common.base.Preconditions; +import com.sequence.anonymous.invite.domain.Invite; +import com.sequence.anonymous.invite.domain.Kind; +import com.sequence.anonymous.invite.domain.Status; +import com.sequence.anonymous.user.domain.user.User; + +public record InviteResponse (Long id, User inviter, User invitee, Kind kind, Status status){ + + public InviteResponse { + Preconditions.checkArgument(id != null, "id must be provided"); + Preconditions.checkArgument(inviter!=null, "inviter must be provided"); + Preconditions.checkArgument(invitee!=null, "invitee must be provided"); + Preconditions.checkArgument(kind!=null, "kind must be provided"); + Preconditions.checkArgument(status != null, "status must be provided"); + } + + public static InviteResponse fromInvite(Invite invite) { + Preconditions.checkArgument(invite != null); + + return new InviteResponse(invite.getId(), invite.getInviter(), invite.getInvitee(), invite.getKind(), + invite.getStatus()); + } +} diff --git a/src/main/java/com/sequence/anonymous/security/CustomOAuth2User.java b/src/main/java/com/sequence/anonymous/security/CustomOAuth2User.java index e715a95..7e947fd 100644 --- a/src/main/java/com/sequence/anonymous/security/CustomOAuth2User.java +++ b/src/main/java/com/sequence/anonymous/security/CustomOAuth2User.java @@ -6,11 +6,13 @@ import java.util.Collection; import java.util.Map; import java.util.Objects; +import lombok.Getter; import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringStyle; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.oauth2.core.user.OAuth2User; +@Getter public class CustomOAuth2User implements OAuth2User, Serializable { private final Long id;