Skip to content

Commit fdd764b

Browse files
authored
feat: 친구 삭제/닉네임 부분 검색 추가 (#249)
* bug: EnableScheduling 제거 * bug: 젠킨스 실행 오류 해결 * bug: 젠킨스 실행 오류 해결 * feat: 친구 보낸 요청 API 추가 * feat: 친구 삭제/닉네임 검색 추가
1 parent cbcdbb3 commit fdd764b

File tree

10 files changed

+179
-13
lines changed

10 files changed

+179
-13
lines changed

src/main/java/ku_rum/backend/domain/friend/application/FriendManageService.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,4 +63,19 @@ public void deleteSentRequest(final FriendRequest friendSendRequest) {
6363
friendRepository.delete(friend);
6464
}
6565

66+
public void deleteFriend(Long targetUserId) {
67+
User currentUser = userUtil.getUser();
68+
User targetUser = userQueryService.getUserById(targetUserId);
69+
70+
boolean isFriend = friendRepository.existsByFromUserAndToUserAndStatus(currentUser, targetUser, FriendStatus.ACCEPT) ||
71+
friendRepository.existsByFromUserAndToUserAndStatus(targetUser, currentUser, FriendStatus.ACCEPT);
72+
73+
if (!isFriend) {
74+
throw new GlobalException(NO_FRIEND_REQUEST);
75+
}
76+
77+
friendRepository.deleteByFromUserAndToUser(currentUser, targetUser);
78+
friendRepository.deleteByFromUserAndToUser(targetUser, currentUser);
79+
}
80+
6681
}

src/main/java/ku_rum/backend/domain/friend/application/FriendQueryService.java

Lines changed: 42 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44
import ku_rum.backend.domain.friend.domain.repository.FriendRepository;
55
import ku_rum.backend.domain.friend.domain.vo.FriendStatus;
66
import ku_rum.backend.domain.friend.dto.response.FriendListResponse;
7+
import ku_rum.backend.domain.friend.dto.response.FriendSearchResponse;
78
import ku_rum.backend.domain.friend.dto.response.ReceivedFriendResponse;
89
import ku_rum.backend.domain.friend.dto.response.SentFriendResponse;
910
import ku_rum.backend.domain.user.domain.User;
11+
import ku_rum.backend.domain.user.domain.repository.UserRepository;
1012
import ku_rum.backend.global.utill.UserUtil;
1113
import lombok.RequiredArgsConstructor;
1214
import org.springframework.stereotype.Service;
@@ -22,6 +24,7 @@
2224
public class FriendQueryService {
2325

2426
private final FriendRepository friendRepository;
27+
private final UserRepository userRepository;
2528
private final UserUtil userUtil;
2629

2730
// 친구 목록 조회
@@ -48,16 +51,43 @@ public List<ReceivedFriendResponse> getReceivedPendingRequests() {
4851
}
4952

5053
public List<SentFriendResponse> getSentPendingRequests() {
51-
User currentUser = userUtil.getUser();
52-
List<Friend> sentRequests = friendRepository.findByFromUserAndStatus(currentUser, FriendStatus.PENDING);
53-
54-
return sentRequests.stream()
55-
.map(req -> new SentFriendResponse(
56-
req.getId(),
57-
req.getToUser().getId(),
58-
req.getToUser().getNickname(),
59-
req.getToUser().getImageUrl()
60-
))
61-
.toList();
62-
}
54+
User currentUser = userUtil.getUser();
55+
List<Friend> sentRequests = friendRepository.findByFromUserAndStatus(currentUser, FriendStatus.PENDING);
56+
57+
return sentRequests.stream()
58+
.map(req -> new SentFriendResponse(
59+
req.getId(),
60+
req.getToUser().getId(),
61+
req.getToUser().getNickname(),
62+
req.getToUser().getImageUrl()
63+
))
64+
.toList();
65+
}
66+
67+
public List<FriendSearchResponse> searchByNickname(final String nickname) {
68+
User currentUser = userUtil.getUser();
69+
List<User> matchedUsers = userRepository.findByNicknameContainingIgnoreCase(nickname).stream()
70+
.filter(user -> !user.getId().equals(currentUser.getId()))
71+
.toList();
72+
73+
List<Long> targetUserIds = matchedUsers.stream()
74+
.map(User::getId)
75+
.collect(Collectors.toList());
76+
77+
// 1. 친구 요청을 보낸 사용자 ID 리스트 (PENDING)
78+
List<Long> sentRequestUserIds = friendRepository.findToUserIdsByFromUserAndStatus(currentUser.getId(), targetUserIds, FriendStatus.PENDING);
79+
80+
// 2. 친구인 사용자 ID 리스트 (ACCEPTED 양방향)
81+
List<Long> friendUserIds = friendRepository.findFriendUserIds(currentUser.getId(), targetUserIds, FriendStatus.ACCEPT);
82+
83+
return matchedUsers.stream()
84+
.map(user -> new FriendSearchResponse(
85+
user.getId(),
86+
user.getNickname(),
87+
user.getImageUrl(),
88+
sentRequestUserIds.contains(user.getId()),
89+
friendUserIds.contains(user.getId())
90+
))
91+
.collect(Collectors.toList());
92+
}
6393
}

src/main/java/ku_rum/backend/domain/friend/domain/repository/FriendRepository.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
import ku_rum.backend.domain.friend.domain.vo.FriendStatus;
55
import ku_rum.backend.domain.user.domain.User;
66
import org.springframework.data.jpa.repository.JpaRepository;
7+
import org.springframework.data.jpa.repository.Query;
8+
import org.springframework.data.repository.query.Param;
79
import org.springframework.stereotype.Repository;
810

911
import java.util.List;
@@ -16,4 +18,22 @@ public interface FriendRepository extends JpaRepository<Friend, Long> {
1618
Optional<Friend> findByFromUserAndToUser(User fromUser, User toUser);
1719
List<Friend> findByToUserAndStatus(User toUser, FriendStatus status);
1820
List<Friend> findByFromUserAndStatus(User fromUser, FriendStatus status);
21+
22+
@Query("SELECT f.toUser.id FROM Friend f " +
23+
"WHERE f.fromUser.id = :fromUserId AND f.toUser.id IN :targetUserIds AND f.status = :status")
24+
List<Long> findToUserIdsByFromUserAndStatus(@Param("fromUserId") Long fromUserId,
25+
@Param("targetUserIds") List<Long> targetUserIds,
26+
@Param("status") FriendStatus status);
27+
28+
@Query("SELECT f.toUser.id FROM Friend f " +
29+
"WHERE f.fromUser.id = :currentUserId AND f.toUser.id IN :targetUserIds AND f.status = :status " +
30+
"UNION " +
31+
"SELECT f.fromUser.id FROM Friend f " +
32+
"WHERE f.toUser.id = :currentUserId AND f.fromUser.id IN :targetUserIds AND f.status = :status")
33+
List<Long> findFriendUserIds(@Param("currentUserId") Long currentUserId,
34+
@Param("targetUserIds") List<Long> targetUserIds,
35+
@Param("status") FriendStatus status);
36+
37+
boolean existsByFromUserAndToUserAndStatus(User fromUser, User toUser, FriendStatus status);
38+
1939
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
package ku_rum.backend.domain.friend.dto.response;
2+
3+
public record FriendSearchResponse(Long userId, String nickname, String imageUrl, boolean requestSent, boolean isFriend) {
4+
}

src/main/java/ku_rum/backend/domain/friend/presentation/FriendManageController.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,4 +48,13 @@ public BaseResponse<Void> deleteSentRequest(@RequestBody final FriendRequest Fri
4848
friendManageService.deleteSentRequest(FriendDeleteRequest);
4949
return BaseResponse.ok();
5050
}
51+
52+
/**
53+
* 친구 삭제 API
54+
*/
55+
@DeleteMapping("/{friendId}")
56+
public BaseResponse<Void> deleteFriend(@PathVariable final Long friendId) {
57+
friendManageService.deleteFriend(friendId);
58+
return BaseResponse.ok();
59+
}
5160
}

src/main/java/ku_rum/backend/domain/friend/presentation/FriendQueryController.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@
22

33
import ku_rum.backend.domain.friend.application.FriendQueryService;
44
import ku_rum.backend.domain.friend.dto.response.FriendListResponse;
5+
import ku_rum.backend.domain.friend.dto.response.FriendSearchResponse;
56
import ku_rum.backend.domain.friend.dto.response.ReceivedFriendResponse;
67
import ku_rum.backend.domain.friend.dto.response.SentFriendResponse;
78
import ku_rum.backend.global.support.response.BaseResponse;
89
import lombok.RequiredArgsConstructor;
910
import org.springframework.web.bind.annotation.GetMapping;
1011
import org.springframework.web.bind.annotation.RequestMapping;
12+
import org.springframework.web.bind.annotation.RequestParam;
1113
import org.springframework.web.bind.annotation.RestController;
1214

1315
import java.util.List;
@@ -32,4 +34,9 @@ public BaseResponse<List<ReceivedFriendResponse>> getReceivedRequests() {
3234
public BaseResponse<List<SentFriendResponse>> getSentRequests() {
3335
return BaseResponse.ok(friendQueryService.getSentPendingRequests());
3436
}
37+
38+
@GetMapping("/search")
39+
public BaseResponse<List<FriendSearchResponse>> searchFriendByNickname(@RequestParam final String nickname) {
40+
return BaseResponse.ok(friendQueryService.searchByNickname(nickname));
41+
}
3542
}

src/main/java/ku_rum/backend/domain/user/domain/repository/UserRepository.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import org.springframework.data.jpa.repository.JpaRepository;
55
import org.springframework.stereotype.Repository;
66

7+
import java.util.List;
78
import java.util.Optional;
89

910
@Repository
@@ -25,4 +26,6 @@ public interface UserRepository extends JpaRepository<User, Long> {
2526
Optional<User> findUserByLoginId(String loginId);
2627

2728
Optional<User> findByOauthId(String oauthId);
29+
30+
List<User> findByNicknameContainingIgnoreCase(String nickname);
2831
}

src/main/java/ku_rum/backend/global/support/status/BaseExceptionResponseStatus.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ public enum BaseExceptionResponseStatus implements ResponseStatus {
7777
NO_PENDING_LIST(706, HttpStatus.BAD_REQUEST, "친구 요청에 목록이 없습니다."),
7878
NOT_EQUAL_TO_USER(707, HttpStatus.BAD_REQUEST, "보낸 요청과 친구가 일치하지 않습니다."),
7979
DUPLICATE_RESPONSE(708, HttpStatus.BAD_REQUEST, "이미 처리된 요청은 삭제할 수 없습니다."),
80+
NO_FRIEND_REQUEST(709, HttpStatus.BAD_REQUEST, "해당 친구가 존재하지 않습니다"),
8081

8182
/**
8283
* 800: Notice

src/test/java/ku_rum/backend/domain/friend/presentation/FriendManageControllerTest.java

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName;
2323
import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.*;
2424
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
25+
import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName;
26+
2527
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
2628
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
2729

@@ -158,4 +160,35 @@ void deleteFriendRequest() throws Exception {
158160
.build())
159161
));
160162
}
163+
164+
@Test
165+
@DisplayName("친구 삭제 API")
166+
@WithMockUser
167+
void deleteFriend() throws Exception {
168+
Long friendId = 1L;
169+
170+
// friendManageService.deleteFriend(friendId)를 void로 호출하는 경우
171+
doNothing().when(friendManageService).deleteFriend(friendId);
172+
173+
mockMvc.perform(delete("/api/v1/friends/{friendId}", friendId)
174+
.header("Authorization", "Bearer your.jwt.token"))
175+
.andDo(print())
176+
.andExpect(status().isOk())
177+
.andExpectAll(RestDocsTestUtils.expectCommonSuccess())
178+
.andDo(restDocs.document(
179+
resource(ResourceSnippetParameters.builder()
180+
.tag("친구 관련 API")
181+
.description("친구 삭제")
182+
.requestHeaders(
183+
headerWithName("Authorization").description("발급 받은 액세스 토큰")
184+
)
185+
.pathParameters(
186+
parameterWithName("friendId").description("삭제할 친구의 사용자 ID")
187+
)
188+
.responseFields(RestDocsFieldSnippets.COMMON_RESPONSE_FIELDS)
189+
.build())
190+
));
191+
192+
}
193+
161194
}

src/test/java/ku_rum/backend/domain/friend/presentation/FriendQueryControllerTest.java

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import ku_rum.backend.config.RestDocsTestSupport;
55
import ku_rum.backend.domain.friend.application.FriendQueryService;
66
import ku_rum.backend.domain.friend.dto.response.FriendListResponse;
7+
import ku_rum.backend.domain.friend.dto.response.FriendSearchResponse;
78
import ku_rum.backend.domain.friend.dto.response.ReceivedFriendResponse;
89
import ku_rum.backend.domain.friend.dto.response.SentFriendResponse;
910
import ku_rum.backend.global.batch.BatchScheduler;
@@ -27,6 +28,7 @@
2728
import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName;
2829
import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get;
2930
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
31+
import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName;
3032
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
3133
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
3234
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@@ -125,7 +127,7 @@ void getReceivedFriendRequests() throws Exception {
125127
void getSentRequests() throws Exception {
126128
// given
127129
List<SentFriendResponse> mockResponse = List.of(
128-
new SentFriendResponse(1L, 2L, "receiver1","image"),
130+
new SentFriendResponse(1L, 2L, "receiver1", "image"),
129131
new SentFriendResponse(2L, 3L, "receiver2", "image2")
130132
);
131133

@@ -158,6 +160,48 @@ void getSentRequests() throws Exception {
158160
));
159161
}
160162

163+
@Test
164+
@DisplayName("닉네임으로 친구 검색 API")
165+
@WithMockUser
166+
void searchFriendByNickname() throws Exception {
167+
String nickname = "minu";
168+
169+
// Mock 응답 데이터
170+
List<FriendSearchResponse> mockResponse = List.of(
171+
new FriendSearchResponse(1L, "minu1", "https://img1.com", true, true),
172+
new FriendSearchResponse(2L, "minu2", "https://img2.com", false, false)
173+
);
174+
when(friendQueryService.searchByNickname(nickname)).thenReturn(mockResponse);
175+
176+
mockMvc.perform(get("/api/v1/friends/search")
177+
.header("Authorization", "Bearer your.jwt.token")
178+
.param("nickname", nickname))
179+
.andDo(print())
180+
.andExpect(status().isOk())
181+
.andExpectAll(RestDocsTestUtils.expectCommonSuccess())
182+
.andDo(restDocs.document(
183+
resource(ResourceSnippetParameters.builder()
184+
.tag("친구 관련 API")
185+
.description("닉네임으로 친구 검색")
186+
.requestHeaders(
187+
headerWithName("Authorization").description("발급 받은 액세스 토큰")
188+
)
189+
.queryParameters(
190+
parameterWithName("nickname").description("검색할 닉네임 (부분일치)")
191+
)
192+
.responseFields(
193+
RestDocsFieldSnippets.COMMON_RESPONSE_FIELDS
194+
)
195+
.responseFields(RestDocsFieldSnippets.withDataFields(List.of(
196+
fieldWithPath("data[].userId").type(JsonFieldType.NUMBER).description("유저 ID"),
197+
fieldWithPath("data[].nickname").type(JsonFieldType.STRING).description("유저 닉네임"),
198+
fieldWithPath("data[].imageUrl").type(JsonFieldType.STRING).description("프로필 이미지 URL"),
199+
fieldWithPath("data[].requestSent").type(JsonFieldType.BOOLEAN).description("친구 요청 보냈는지 여부"),
200+
fieldWithPath("data[].isFriend").type(JsonFieldType.BOOLEAN).description("이미 친구인지 여부")
201+
)))
202+
.build())
203+
));
204+
}
161205

162206

163207
}

0 commit comments

Comments
 (0)