Skip to content

Commit 8a82649

Browse files
authored
Merge pull request #83 from sgdevcamp2025/be/faet/81-friends-crud
�[BE] feat: 친구 관련 API
2 parents 45e4b8c + c003531 commit 8a82649

File tree

18 files changed

+709
-2
lines changed

18 files changed

+709
-2
lines changed

src/backend/user-server/build.gradle

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,13 @@ dependencies {
4646
testImplementation 'org.springframework.boot:spring-boot-starter-test'
4747
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
4848

49+
// queryDSL
50+
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
51+
annotationProcessor "jakarta.persistence:jakarta.persistence-api"
52+
53+
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
54+
annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta"
55+
4956
// Security
5057
implementation 'org.springframework.boot:spring-boot-starter-security'
5158
implementation 'org.springframework.security:spring-security-oauth2-client'
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package com.asyncgate.user_server.config;
2+
3+
import com.querydsl.jpa.impl.JPAQueryFactory;
4+
import jakarta.persistence.EntityManager;
5+
import jakarta.persistence.PersistenceContext;
6+
import org.springframework.context.annotation.Bean;
7+
import org.springframework.context.annotation.Configuration;
8+
9+
@Configuration
10+
public class QuerydslConfig {
11+
12+
@PersistenceContext
13+
private EntityManager entityManager;
14+
15+
@Bean
16+
public JPAQueryFactory jpaQueryFactory() {
17+
return new JPAQueryFactory(entityManager);
18+
}
19+
}
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
package com.asyncgate.user_server.controller;
2+
3+
import com.asyncgate.user_server.controller.docs.FriendControllerDocs;
4+
import com.asyncgate.user_server.domain.Friend;
5+
import com.asyncgate.user_server.domain.Member;
6+
import com.asyncgate.user_server.dto.response.FriendResponse;
7+
import com.asyncgate.user_server.dto.response.FriendsResponse;
8+
import com.asyncgate.user_server.dto.response.MemberResponse;
9+
import com.asyncgate.user_server.security.annotation.MemberID;
10+
import com.asyncgate.user_server.support.response.SuccessResponse;
11+
import com.asyncgate.user_server.usecase.FriendUseCase;
12+
import lombok.RequiredArgsConstructor;
13+
import org.springframework.web.bind.annotation.*;
14+
15+
import java.util.List;
16+
17+
@RestController
18+
@RequiredArgsConstructor
19+
@RequestMapping("/friends")
20+
public class FriendController implements FriendControllerDocs {
21+
22+
private final FriendUseCase friendUseCase;
23+
24+
/**
25+
* 회원 검색 (이메일 기준)
26+
*/
27+
@Override
28+
@GetMapping
29+
public SuccessResponse<MemberResponse> searchTarget(final @RequestParam String email) {
30+
Member findMember = friendUseCase.getByEmail(email);
31+
return SuccessResponse.ok(
32+
MemberResponse.from(findMember)
33+
);
34+
}
35+
36+
/**
37+
* 친구 요청: 현재 사용자(userId)가 toUserId에게 친구 요청
38+
* URL 예시: POST /friends/request/{toUserId}
39+
*/
40+
@Override
41+
@PostMapping("/request/{toUserId}")
42+
public SuccessResponse<FriendResponse> requestFriend(
43+
final @MemberID String userId,
44+
final @PathVariable String toUserId
45+
) {
46+
Friend friend = friendUseCase.registerFriend(userId, toUserId);
47+
return SuccessResponse.created(
48+
FriendResponse.from(friend)
49+
);
50+
}
51+
52+
/**
53+
* 친구 수락: 현재 사용자(userId)가 friendId에 해당하는 친구 요청을 수락
54+
* URL 예시: POST /friends/accept/{friendId}
55+
*/
56+
@Override
57+
@PostMapping("/accept/{friendId}")
58+
public SuccessResponse<FriendResponse> acceptFriend(
59+
final @MemberID String userId,
60+
final @PathVariable String friendId
61+
) {
62+
Friend friend = friendUseCase.acceptFriend(userId, friendId);
63+
return SuccessResponse.ok(
64+
FriendResponse.from(friend)
65+
);
66+
}
67+
68+
/**
69+
* 친구 거절: 현재 사용자(userId)가 friendId에 해당하는 친구 요청을 거절
70+
* URL 예시: POST /friends/reject/{friendId}
71+
*/
72+
@Override
73+
@PostMapping("/reject/{friendId}")
74+
public SuccessResponse<FriendResponse> rejectFriend(
75+
final @MemberID String userId,
76+
@PathVariable String friendId) {
77+
Friend friend = friendUseCase.rejectFriend(userId, friendId);
78+
return SuccessResponse.ok(FriendResponse.from(friend));
79+
}
80+
81+
/**
82+
* 친구 삭제(soft delete): 현재 사용자(userId)가 friendId에 해당하는 친구 관계를 soft delete 처리
83+
* URL 예시: DELETE /friends/{friendId}
84+
*/
85+
@Override
86+
@DeleteMapping("/{friendId}")
87+
public SuccessResponse<String> deleteFriend(
88+
final @MemberID String userId,
89+
final @PathVariable String friendId
90+
) {
91+
friendUseCase.deleteFriend(userId, friendId);
92+
return SuccessResponse.ok(String.format("UserId[&s]인 친구를 삭제했습니다.", friendId));
93+
}
94+
95+
/**
96+
* 본인이 보낸 친구 요청 목록 조회 (상태: PENDING)
97+
* URL 예시: GET /friends/sent
98+
*/
99+
@Override
100+
@GetMapping("/sent")
101+
public SuccessResponse<FriendsResponse> getSentFriendRequests(final @MemberID String userId) {
102+
List<Friend> sent = friendUseCase.getSentFriendRequests(userId);
103+
return SuccessResponse.ok(
104+
FriendsResponse.from(sent)
105+
);
106+
}
107+
108+
/**
109+
* 본인이 받은 친구 요청 목록 조회 (상태: PENDING)
110+
* URL 예시: GET /friends/received
111+
*/
112+
@Override
113+
@GetMapping("/received")
114+
public SuccessResponse<FriendsResponse> getReceivedFriendRequests(final @MemberID String userId) {
115+
List<Friend> received = friendUseCase.getReceivedFriendRequests(userId);
116+
return SuccessResponse.ok(
117+
FriendsResponse.from(received)
118+
);
119+
}
120+
121+
/**
122+
* 본인의 실제 친구 목록 조회 (상태: ACCEPTED)
123+
* URL 예시: GET /friends/list
124+
*/
125+
@Override
126+
@GetMapping("/list")
127+
public SuccessResponse<FriendsResponse> getFriends(final @MemberID String userId) {
128+
List<Friend> friends = friendUseCase.getFriends(userId);
129+
return SuccessResponse.ok(
130+
FriendsResponse.from(friends)
131+
);
132+
}
133+
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package com.asyncgate.user_server.controller.docs;
2+
3+
import com.asyncgate.user_server.dto.response.FriendResponse;
4+
import com.asyncgate.user_server.dto.response.FriendsResponse;
5+
import com.asyncgate.user_server.dto.response.MemberResponse;
6+
import com.asyncgate.user_server.security.annotation.MemberID;
7+
import com.asyncgate.user_server.support.response.SuccessResponse;
8+
import io.swagger.v3.oas.annotations.Operation;
9+
import io.swagger.v3.oas.annotations.Parameter;
10+
import io.swagger.v3.oas.annotations.responses.ApiResponse;
11+
import io.swagger.v3.oas.annotations.responses.ApiResponses;
12+
import org.springframework.web.bind.annotation.*;
13+
14+
public interface FriendControllerDocs {
15+
16+
@Operation(summary = "회원 검색", description = "이메일을 기반으로 회원 정보를 조회합니다.")
17+
@ApiResponses(value = {
18+
@ApiResponse(responseCode = "200", description = "정상적으로 조회되었습니다.")
19+
})
20+
@GetMapping
21+
SuccessResponse<MemberResponse> searchTarget(
22+
@Parameter(description = "검색할 회원의 이메일", required = true)
23+
@RequestParam String email
24+
);
25+
26+
27+
@Operation(summary = "친구 요청", description = "현재 사용자(@MemberID)가 지정된 toUserId에게 친구 요청을 보냅니다.")
28+
@ApiResponses(value = {
29+
@ApiResponse(responseCode = "201", description = "친구 요청이 생성되었습니다.")
30+
})
31+
@PostMapping("/request/{toUserId}")
32+
SuccessResponse<FriendResponse> requestFriend(
33+
@Parameter(hidden = true) @MemberID String userId,
34+
@Parameter(description = "친구 요청 대상의 사용자 ID", required = true)
35+
@PathVariable String toUserId
36+
);
37+
38+
39+
@Operation(summary = "친구 요청 수락", description = "현재 사용자(@MemberID)가 friendId에 해당하는 친구 요청을 수락합니다.")
40+
@ApiResponses(value = {
41+
@ApiResponse(responseCode = "200", description = "친구 요청이 수락되었습니다.")
42+
})
43+
@PostMapping("/accept/{friendId}")
44+
SuccessResponse<FriendResponse> acceptFriend(
45+
@Parameter(hidden = true) @MemberID String userId,
46+
@Parameter(description = "수락할 친구 요청의 ID", required = true)
47+
@PathVariable String friendId
48+
);
49+
50+
51+
@Operation(summary = "친구 요청 거절", description = "현재 사용자(@MemberID)가 friendId에 해당하는 친구 요청을 거절합니다.")
52+
@ApiResponses(value = {
53+
@ApiResponse(responseCode = "200", description = "친구 요청이 거절되었습니다.")
54+
})
55+
@PostMapping("/reject/{friendId}")
56+
SuccessResponse<FriendResponse> rejectFriend(
57+
@Parameter(hidden = true) @MemberID String userId,
58+
@Parameter(description = "거절할 친구 요청의 ID", required = true)
59+
@PathVariable String friendId
60+
);
61+
62+
63+
@Operation(summary = "친구 삭제", description = "현재 사용자(@MemberID)가 friendId에 해당하는 친구 관계를 삭제(soft delete)합니다.")
64+
@ApiResponses(value = {
65+
@ApiResponse(responseCode = "200", description = "친구 관계가 삭제되었습니다.")
66+
})
67+
@DeleteMapping("/{friendId}")
68+
SuccessResponse<String> deleteFriend(
69+
@Parameter(hidden = true) @MemberID String userId,
70+
@Parameter(description = "삭제할 친구 관계의 ID", required = true)
71+
@PathVariable String friendId
72+
);
73+
74+
75+
@Operation(summary = "보낸 친구 요청 목록 조회", description = "현재 사용자(@MemberID)가 보낸 친구 요청 목록(PENDING)을 조회합니다.")
76+
@ApiResponses(value = {
77+
@ApiResponse(responseCode = "200", description = "보낸 친구 요청 목록이 조회되었습니다.")
78+
})
79+
@GetMapping("/sent")
80+
SuccessResponse<FriendsResponse> getSentFriendRequests(
81+
@Parameter(hidden = true) @MemberID String userId
82+
);
83+
84+
85+
@Operation(summary = "받은 친구 요청 목록 조회", description = "현재 사용자(@MemberID)가 받은 친구 요청 목록(PENDING)을 조회합니다.")
86+
@ApiResponses(value = {
87+
@ApiResponse(responseCode = "200", description = "받은 친구 요청 목록이 조회되었습니다.")
88+
})
89+
@GetMapping("/received")
90+
SuccessResponse<FriendsResponse> getReceivedFriendRequests(
91+
@Parameter(hidden = true) @MemberID String userId
92+
);
93+
94+
95+
@Operation(summary = "친구 목록 조회", description = "현재 사용자(@MemberID)의 실제 친구 목록(ACCEPTED)을 조회합니다.")
96+
@ApiResponses(value = {
97+
@ApiResponse(responseCode = "200", description = "친구 목록이 조회되었습니다.")
98+
})
99+
@GetMapping("/list")
100+
SuccessResponse<FriendsResponse> getFriends(
101+
@Parameter(hidden = true) @MemberID String userId
102+
);
103+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package com.asyncgate.user_server.domain;
2+
3+
import lombok.Getter;
4+
5+
import java.util.UUID;
6+
7+
@Getter
8+
public class Friend {
9+
private final String id;
10+
private final String userId1;
11+
private final String userId2;
12+
private final String requestedBy;
13+
private FriendStatus status;
14+
15+
private Friend(String id, String userId1, String userId2, String requestedBy, FriendStatus status) {
16+
this.id = id;
17+
this.userId1 = userId1;
18+
this.userId2 = userId2;
19+
this.requestedBy = requestedBy;
20+
this.status = status;
21+
}
22+
23+
public static Friend create(String requestUserId, String toUserId) {
24+
// 두 사용자 ID를 사전 순으로 정렬하여 저장
25+
if (requestUserId.compareTo(toUserId) < 0) {
26+
return new Friend(UUID.randomUUID().toString(), requestUserId, toUserId, requestUserId, FriendStatus.PENDING);
27+
} else {
28+
return new Friend(UUID.randomUUID().toString(), toUserId, requestUserId, requestUserId, FriendStatus.PENDING);
29+
}
30+
}
31+
32+
public static Friend of(String id, String userId1, String userId2, String requestedBy, FriendStatus status) {
33+
return new Friend(id, userId1, userId2, requestedBy, status);
34+
}
35+
36+
public void accept() {
37+
this.status = FriendStatus.ACCEPTED;
38+
}
39+
40+
public void reject() {
41+
this.status = FriendStatus.REJECTED;
42+
}
43+
44+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.asyncgate.user_server.domain;
2+
3+
public enum FriendStatus {
4+
PENDING,
5+
ACCEPTED,
6+
REJECTED
7+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package com.asyncgate.user_server.dto.response;
2+
3+
import com.asyncgate.user_server.domain.Friend;
4+
import com.asyncgate.user_server.domain.FriendStatus;
5+
6+
public record FriendResponse(String id, String userId1, String userId2, String requestedBy, FriendStatus status) {
7+
public static FriendResponse from(final Friend friend) {
8+
return new FriendResponse(
9+
friend.getId(),
10+
friend.getUserId1(),
11+
friend.getUserId2(),
12+
friend.getRequestedBy(),
13+
friend.getStatus()
14+
);
15+
}
16+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package com.asyncgate.user_server.dto.response;
2+
3+
import com.asyncgate.user_server.domain.Friend;
4+
5+
import java.util.List;
6+
7+
public record FriendsResponse(List<FriendResponse> friends) {
8+
public static FriendsResponse from(final List<Friend> friendList) {
9+
List<FriendResponse> responses = friendList.stream()
10+
.map(FriendResponse::from)
11+
.toList();
12+
return new FriendsResponse(responses);
13+
}
14+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.asyncgate.user_server.dto.response;
2+
3+
import com.asyncgate.user_server.domain.Member;
4+
5+
import java.time.LocalDate;
6+
7+
public record MemberResponse(String email, String name, String nickname, String profileImgUrl, LocalDate birth) {
8+
9+
public static MemberResponse from(Member member) {
10+
return new MemberResponse(member.getEmail(), member.getName(), member.getNickname(), member.getProfileImgUrl(), member.getBirth());
11+
}
12+
13+
}

0 commit comments

Comments
 (0)