Skip to content
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

[LIME-9] 친구 관계 기능 추가 #15

Merged
merged 20 commits into from
Jan 23, 2024
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
e761c40
feat: 팔로우 기능 추가
Yiseull Jan 15, 2024
bcf1627
feat: 언팔로우 기능 추가
Yiseull Jan 15, 2024
a321c86
fix: 팔로워, 팔로잉 거꾸로 저장되는 버그 수정
Yiseull Jan 17, 2024
870b237
feat: 팔로워 목록 조회 기능 추가
Yiseull Jan 17, 2024
9f2325d
refactor: 팔로워 목록 dto명 변경
Yiseull Jan 17, 2024
0163567
refactor: 팔로워 목록 조회 메서드명 변경
Yiseull Jan 17, 2024
77eb08b
feat: 팔로잉 목록 조회 기능 추가
Yiseull Jan 17, 2024
4241aae
refactor: 팔로우 목록 조회랑 팔로워 목록 조회의 공통 로직을 메서드로 추출
Yiseull Jan 17, 2024
e5892ee
fix: 소셜로그인 추가로 인한 변경
Yiseull Jan 17, 2024
7669afd
feat: 마이페이지 조회에 팔로워 수, 팔로잉 수 추가
Yiseull Jan 18, 2024
8fd822b
fix: 팔로우 할 때 이미 친구인 경우 계속 친구 관계가 추가되는 버그 수정
Yiseull Jan 21, 2024
d01ac33
feat: Friendship에서 toMember, fromMember 객체참조로 변경
Yiseull Jan 21, 2024
0b6013e
test: 팔로우 테스트 추가
Yiseull Jan 21, 2024
1ff2a14
fix: rebase main 할 때 생긴 오류 해결
Yiseull Jan 21, 2024
e8342e2
test: 팔로워 수 조회 테스트, 팔로잉 수 조회 테스트 추가
Yiseull Jan 21, 2024
55c0c55
test: 언팔로우 테스트 추가
Yiseull Jan 21, 2024
0e734f5
rename: FriendshipBuilder 패키지 이동
Yiseull Jan 21, 2024
dc0ef95
test: 팔로워 조회, 팔로잉 조회, 친구 관계 조회 테스트 추가
Yiseull Jan 21, 2024
b050686
refactor: stream().forEach() -> forEach()
Yiseull Jan 21, 2024
6197520
comment: 팔로워, 팔로잉 목록 조회 설명 변경
Yiseull Jan 23, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package com.programmers.lime.domains.friendships.api;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
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.RestController;

import com.programmers.lime.common.cursor.CursorSummary;
import com.programmers.lime.domains.friendships.api.dto.response.FriendshipGetByCursorResponse;
import com.programmers.lime.domains.friendships.application.FriendshipService;
import com.programmers.lime.domains.friendships.model.FriendshipSummary;
import com.programmers.lime.global.cursor.CursorRequest;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;

@Tag(name = "friendships", description = "친구 관계 API")
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/friendships")
public class FriendshipController {

private final FriendshipService friendshipService;

@Operation(summary = "팔로우", description = "팔로우 할 닉네임을 이용하여 팔로우 합니다.")
@PostMapping("/follow/{nickname}")
public ResponseEntity<Void> follow(@PathVariable final String nickname) {
friendshipService.follow(nickname);

return ResponseEntity.ok().build();
}

@Operation(summary = "언팔로우", description = "언팔로우 할 닉네임을 이용하여 팔로우를 취소합니다.")
@PostMapping("/unfollow/{nickname}")
public ResponseEntity<Void> unfollow(@PathVariable final String nickname) {
friendshipService.unfollow(nickname);

return ResponseEntity.ok().build();
}

@Operation(summary = "팔로워 목록 조회", description = "회원을 팔로우한 사람들의 목록을 조회한다.")
@GetMapping("/follower/{nickname}")
public ResponseEntity<FriendshipGetByCursorResponse> getFollower(
@PathVariable final String nickname,
@ModelAttribute @Valid final CursorRequest request
) {
final CursorSummary<FriendshipSummary> cursorSummary = friendshipService.getFollower(
nickname,
request.toParameters()
);
final FriendshipGetByCursorResponse response = FriendshipGetByCursorResponse.from(cursorSummary);

return ResponseEntity.ok(response);
}

@Operation(summary = "팔로잉 목록 조회", description = "회원이 팔로우한 사람들의 목록을 조회한다.")
@GetMapping("/following/{nickname}")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

설명 팔로잉으로 고치면 될거같아욤

Copy link
Member Author

@Yiseull Yiseull Jan 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

6197520

설명 수정했습니다! 😄

public ResponseEntity<FriendshipGetByCursorResponse> getFollowing(
@PathVariable final String nickname,
@ModelAttribute @Valid final CursorRequest request
) {
final CursorSummary<FriendshipSummary> cursorSummary = friendshipService.getFollowing(
nickname,
request.toParameters()
);
final FriendshipGetByCursorResponse response = FriendshipGetByCursorResponse.from(cursorSummary);

return ResponseEntity.ok(response);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.programmers.lime.domains.friendships.api.dto.response;

import java.util.List;

import com.programmers.lime.common.cursor.CursorSummary;
import com.programmers.lime.domains.friendships.model.FriendshipSummary;

public record FriendshipGetByCursorResponse(
String nextCursorId,
Integer totalCount,
List<FriendshipSummary> followers
) {
public static FriendshipGetByCursorResponse from(final CursorSummary<FriendshipSummary> cursorSummary) {
return new FriendshipGetByCursorResponse(
cursorSummary.nextCursorId(),
cursorSummary.summaryCount(),
cursorSummary.summaries()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.programmers.lime.domains.friendships.application;

import org.springframework.stereotype.Service;

import com.programmers.lime.common.cursor.CursorPageParameters;
import com.programmers.lime.common.cursor.CursorSummary;
import com.programmers.lime.domains.friendships.implementation.FriendshipAppender;
import com.programmers.lime.domains.friendships.implementation.FriendshipReader;
import com.programmers.lime.domains.friendships.implementation.FriendshipRemover;
import com.programmers.lime.domains.friendships.model.FriendshipSummary;
import com.programmers.lime.domains.member.domain.Member;
import com.programmers.lime.domains.member.implementation.MemberReader;
import com.programmers.lime.global.util.MemberUtils;

import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class FriendshipService {

private final FriendshipAppender friendshipAppender;
private final FriendshipRemover friendshipRemover;
private final FriendshipReader friendshipReader;
private final MemberUtils memberUtils;
private final MemberReader memberReader;

public void follow(final String nickname) {
final Member fromMember = memberUtils.getCurrentMember();
final Member toMember = memberReader.readByNickname(nickname);

friendshipAppender.append(toMember, fromMember);
}

public void unfollow(final String nickname) {
final Member fromMember = memberUtils.getCurrentMember();
final Member toMember = memberReader.readByNickname(nickname);

friendshipRemover.remove(toMember, fromMember);
}

public CursorSummary<FriendshipSummary> getFollower(
final String nickname,
final CursorPageParameters parameters
) {
return friendshipReader.readFollowerByCursor(nickname, parameters);
}

public CursorSummary<FriendshipSummary> getFollowing(
final String nickname,
final CursorPageParameters parameters
) {
return friendshipReader.readFollowingByCursor(nickname, parameters);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import com.programmers.lime.domains.auth.OAuth2UserService;
import com.programmers.lime.domains.member.implementation.MemberReader;
import com.programmers.lime.global.config.security.auth.handler.OAuth2LoginFailureHandler;
import com.programmers.lime.global.config.security.auth.handler.OAuth2LoginSuccessHandler;
Expand Down Expand Up @@ -57,6 +58,9 @@ public SecurityFilterChain securityFilterChain(final HttpSecurity http) throws E
.requestMatchers("/api/members/mypage/{nickname}").permitAll()
.requestMatchers("/api/members/refresh").permitAll()

.requestMatchers("/api/friendships/follower/**").permitAll()
.requestMatchers("/api/friendships/following/**").permitAll()

.requestMatchers(HttpMethod.GET, "/api/votes").permitAll()
.requestMatchers(HttpMethod.GET, "/api/votes/{voteId}").permitAll()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ public enum ErrorCode {
MEMBER_NICKNAME_BAD_PATTERN("MEMBER_009", "닉네임은 영어 대소문자, 한글, 숫자 그리고 언더스코어만 허용합니다."),
MEMBER_INTRODUCTION_CONTENT_BAD_LENGTH("MEMBER_012", "자기소개는 최대 300자 입니다."),
MEMBER_INTRODUCTION_MBTI_BAD_REQUEST("MEMBER_014", "잘못된 MBTI 값입니다."),
MEMBER_INTRODUCTION_CAREER_BAD_VALUE("MEMBER_015", "취미 경력은 0보다 작을 수 없습니다."),
MEMBER_INTRODUCTION_FAVORABILITY_BAD_REQUEST("MEMBER_015", "잘못된 선호도 값입니다."),
MEMBER_INTRODUCTION_CAREER_BAD_VALUE("MEMBER_016", "경력은 최소 0개월 이상이어야 합니다."),

// Item
ITEM_MARKET_NOT_FOUND("ITEM_001", "지원하지 않는 아이템 URL 입니다."),
Expand Down Expand Up @@ -107,7 +108,12 @@ public enum ErrorCode {

// MemberItemFolder
MEMBER_ITEM_FOLDER_NOT_FOUND("MEMBER_ITEM_FOLDER_001", "폴더를 찾을 수 없습니다."),
MEMBER_ITEM_FOLDER_NOT_MINE("MEMBER_ITEM_FOLDER_002", "나의 아이템 폴더가 아닙니다.");
MEMBER_ITEM_FOLDER_NOT_MINE("MEMBER_ITEM_FOLDER_002", "나의 아이템 폴더가 아닙니다."),

// Friendship
FRIENDSHIP_NOT_FOUND("FRIENDSHIP_001", "친구 관계를 찾을 수 없습니다."),
FRIENDSHIP_ALREADY_EXISTS("FRIENDSHIP_002", "이미 친구 관계가 존재합니다."),
;

private static final Map<String, ErrorCode> ERROR_CODE_MAP;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.programmers.lime.domains.friendships.domain;

import java.util.Objects;

import com.programmers.lime.domains.BaseEntity;
import com.programmers.lime.domains.member.domain.Member;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
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 jakarta.persistence.Table;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "friendships")
public class Friendship extends BaseEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Long id;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "to_member_id", nullable = false)
private Member toMember;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "from_member_id", nullable = false)
private Member fromMember;
Comment on lines +32 to +38
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

궁금한게 manyToOne으로 연관관계를 맺어줬을 때 발생하는 트러블 슈팅은 없었나요? 맞팔로잉 상태면 순환 관련 문제가 있을 수 있지 않을까 예상했는데 단방향이라 없을 것 같기도 하네요

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

단방향이여서 순환참조 문제는 없을 것 같아요!! 생기면 바로 공유하겠습니다😎


public Friendship(
final Member toMember,
final Member fromMember
) {
this.toMember = Objects.requireNonNull(toMember);
this.fromMember = Objects.requireNonNull(fromMember);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.programmers.lime.domains.friendships.implementation;

import org.springframework.stereotype.Component;

import com.programmers.lime.domains.friendships.domain.Friendship;
import com.programmers.lime.domains.friendships.repository.FriendshipRepository;
import com.programmers.lime.domains.member.domain.Member;
import com.programmers.lime.error.BusinessException;
import com.programmers.lime.error.ErrorCode;

import lombok.RequiredArgsConstructor;

@Component
@RequiredArgsConstructor
public class FriendshipAppender {

private final FriendshipRepository friendshipRepository;

public Friendship append(
final Member toMember,
final Member fromMember
) {
if (friendshipRepository.existsByToMemberAndFromMember(toMember, fromMember)) {
throw new BusinessException(ErrorCode.FRIENDSHIP_ALREADY_EXISTS);
}

final Friendship friendship = new Friendship(toMember, fromMember);

return friendshipRepository.save(friendship);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.programmers.lime.domains.friendships.implementation;

import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import com.programmers.lime.domains.friendships.repository.FriendshipRepository;
import com.programmers.lime.domains.member.domain.Member;

import lombok.RequiredArgsConstructor;

@Component
@RequiredArgsConstructor
public class FriendshipCounter {

private final FriendshipRepository friendshipRepository;

@Transactional(readOnly = true)
public long countFollower(final Member member) {
return friendshipRepository.countByToMember(member);
}

@Transactional(readOnly = true)
public long countFollowing(final Member member) {
return friendshipRepository.countByFromMember(member);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package com.programmers.lime.domains.friendships.implementation;

import java.util.List;

import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import com.programmers.lime.common.cursor.CursorPageParameters;
import com.programmers.lime.common.cursor.CursorSummary;
import com.programmers.lime.common.cursor.CursorUtils;
import com.programmers.lime.domains.friendships.domain.Friendship;
import com.programmers.lime.domains.friendships.model.FriendshipSummary;
import com.programmers.lime.domains.friendships.repository.FriendshipRepository;
import com.programmers.lime.domains.member.domain.Member;
import com.programmers.lime.error.EntityNotFoundException;
import com.programmers.lime.error.ErrorCode;

import lombok.RequiredArgsConstructor;

@Component
@RequiredArgsConstructor
public class FriendshipReader {

public static final int DEFAULT_PAGING_SIZE = 20;

private final FriendshipRepository friendshipRepository;

@Transactional(readOnly = true)
public Friendship read(
final Member toMember,
final Member fromMember
) {
return friendshipRepository.findByToMemberAndFromMember(toMember, fromMember)
.orElseThrow(() -> new EntityNotFoundException(ErrorCode.FRIENDSHIP_NOT_FOUND));
}

@Transactional(readOnly = true)
public CursorSummary<FriendshipSummary> readFollowerByCursor(
final String nickname,
final CursorPageParameters parameters
) {
final int pageSize = getPageSize(parameters);
final List<FriendshipSummary> followerSummaries = friendshipRepository.findFollowerByCursor(
nickname,
parameters.cursorId(),
pageSize
);

return CursorUtils.getCursorSummaries(followerSummaries);
}

@Transactional(readOnly = true)
public CursorSummary<FriendshipSummary> readFollowingByCursor(
final String nickname,
final CursorPageParameters parameters
) {
final int pageSize = getPageSize(parameters);
final List<FriendshipSummary> followerSummaries = friendshipRepository.findFollowingByCursor(
nickname,
parameters.cursorId(),
pageSize
);

return CursorUtils.getCursorSummaries(followerSummaries);
}

private int getPageSize(final CursorPageParameters parameters) {
return parameters.size() == null ? DEFAULT_PAGING_SIZE : parameters.size();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.programmers.lime.domains.friendships.implementation;

import org.springframework.stereotype.Component;

import com.programmers.lime.domains.friendships.domain.Friendship;
import com.programmers.lime.domains.friendships.repository.FriendshipRepository;
import com.programmers.lime.domains.member.domain.Member;

import lombok.RequiredArgsConstructor;

@Component
@RequiredArgsConstructor
public class FriendshipRemover {

private final FriendshipRepository friendshipRepository;
private final FriendshipReader friendshipReader;

public void remove(
final Member toMember,
final Member fromMember
) {
final Friendship friendship = friendshipReader.read(toMember, fromMember);
friendshipRepository.delete(friendship);
}
}
Loading