Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 7 additions & 0 deletions src/backend/user-server/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,13 @@ dependencies {
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'

// queryDSL
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"

implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta"

// Security
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.security:spring-security-oauth2-client'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.asyncgate.user_server.config;

import com.querydsl.jpa.impl.JPAQueryFactory;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class QuerydslConfig {

@PersistenceContext
private EntityManager entityManager;

@Bean
public JPAQueryFactory jpaQueryFactory() {
return new JPAQueryFactory(entityManager);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package com.asyncgate.user_server.controller;

import com.asyncgate.user_server.controller.docs.FriendControllerDocs;
import com.asyncgate.user_server.domain.Friend;
import com.asyncgate.user_server.domain.Member;
import com.asyncgate.user_server.dto.response.FriendResponse;
import com.asyncgate.user_server.dto.response.FriendsResponse;
import com.asyncgate.user_server.dto.response.MemberResponse;
import com.asyncgate.user_server.security.annotation.MemberID;
import com.asyncgate.user_server.support.response.SuccessResponse;
import com.asyncgate.user_server.usecase.FriendUseCase;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequiredArgsConstructor
@RequestMapping("/friends")
public class FriendController implements FriendControllerDocs {

private final FriendUseCase friendUseCase;

/**
* 회원 검색 (이메일 기준)
*/
@Override
@GetMapping
public SuccessResponse<MemberResponse> searchTarget(final @RequestParam String email) {
Member findMember = friendUseCase.getByEmail(email);
return SuccessResponse.ok(
MemberResponse.from(findMember)
);
}

/**
* 친구 요청: 현재 사용자(userId)가 toUserId에게 친구 요청
* URL 예시: POST /friends/request/{toUserId}
*/
@Override
@PostMapping("/request/{toUserId}")
public SuccessResponse<FriendResponse> requestFriend(
final @MemberID String userId,
final @PathVariable String toUserId
) {
Friend friend = friendUseCase.registerFriend(userId, toUserId);
return SuccessResponse.created(
FriendResponse.from(friend)
);
}

/**
* 친구 수락: 현재 사용자(userId)가 friendId에 해당하는 친구 요청을 수락
* URL 예시: POST /friends/accept/{friendId}
*/
@Override
@PostMapping("/accept/{friendId}")
public SuccessResponse<FriendResponse> acceptFriend(
final @MemberID String userId,
final @PathVariable String friendId
) {
Friend friend = friendUseCase.acceptFriend(userId, friendId);
return SuccessResponse.ok(
FriendResponse.from(friend)
);
}

/**
* 친구 거절: 현재 사용자(userId)가 friendId에 해당하는 친구 요청을 거절
* URL 예시: POST /friends/reject/{friendId}
*/
@Override
@PostMapping("/reject/{friendId}")
public SuccessResponse<FriendResponse> rejectFriend(
final @MemberID String userId,
@PathVariable String friendId) {
Friend friend = friendUseCase.rejectFriend(userId, friendId);
return SuccessResponse.ok(FriendResponse.from(friend));
}

/**
* 친구 삭제(soft delete): 현재 사용자(userId)가 friendId에 해당하는 친구 관계를 soft delete 처리
* URL 예시: DELETE /friends/{friendId}
*/
@Override
@DeleteMapping("/{friendId}")
public SuccessResponse<String> deleteFriend(
final @MemberID String userId,
final @PathVariable String friendId
) {
friendUseCase.deleteFriend(userId, friendId);
return SuccessResponse.ok(String.format("UserId[&s]인 친구를 삭제했습니다.", friendId));
}

/**
* 본인이 보낸 친구 요청 목록 조회 (상태: PENDING)
* URL 예시: GET /friends/sent
*/
@Override
@GetMapping("/sent")
public SuccessResponse<FriendsResponse> getSentFriendRequests(final @MemberID String userId) {
List<Friend> sent = friendUseCase.getSentFriendRequests(userId);
return SuccessResponse.ok(
FriendsResponse.from(sent)
);
}

/**
* 본인이 받은 친구 요청 목록 조회 (상태: PENDING)
* URL 예시: GET /friends/received
*/
@Override
@GetMapping("/received")
public SuccessResponse<FriendsResponse> getReceivedFriendRequests(final @MemberID String userId) {
List<Friend> received = friendUseCase.getReceivedFriendRequests(userId);
return SuccessResponse.ok(
FriendsResponse.from(received)
);
}

/**
* 본인의 실제 친구 목록 조회 (상태: ACCEPTED)
* URL 예시: GET /friends/list
*/
@Override
@GetMapping("/list")
public SuccessResponse<FriendsResponse> getFriends(final @MemberID String userId) {
List<Friend> friends = friendUseCase.getFriends(userId);
return SuccessResponse.ok(
FriendsResponse.from(friends)
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package com.asyncgate.user_server.controller.docs;

import com.asyncgate.user_server.dto.response.FriendResponse;
import com.asyncgate.user_server.dto.response.FriendsResponse;
import com.asyncgate.user_server.dto.response.MemberResponse;
import com.asyncgate.user_server.security.annotation.MemberID;
import com.asyncgate.user_server.support.response.SuccessResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import org.springframework.web.bind.annotation.*;

public interface FriendControllerDocs {

@Operation(summary = "회원 검색", description = "이메일을 기반으로 회원 정보를 조회합니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "정상적으로 조회되었습니다.")
})
@GetMapping
SuccessResponse<MemberResponse> searchTarget(
@Parameter(description = "검색할 회원의 이메일", required = true)
@RequestParam String email
);


@Operation(summary = "친구 요청", description = "현재 사용자(@MemberID)가 지정된 toUserId에게 친구 요청을 보냅니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "201", description = "친구 요청이 생성되었습니다.")
})
@PostMapping("/request/{toUserId}")
SuccessResponse<FriendResponse> requestFriend(
@Parameter(hidden = true) @MemberID String userId,
@Parameter(description = "친구 요청 대상의 사용자 ID", required = true)
@PathVariable String toUserId
);


@Operation(summary = "친구 요청 수락", description = "현재 사용자(@MemberID)가 friendId에 해당하는 친구 요청을 수락합니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "친구 요청이 수락되었습니다.")
})
@PostMapping("/accept/{friendId}")
SuccessResponse<FriendResponse> acceptFriend(
@Parameter(hidden = true) @MemberID String userId,
@Parameter(description = "수락할 친구 요청의 ID", required = true)
@PathVariable String friendId
);


@Operation(summary = "친구 요청 거절", description = "현재 사용자(@MemberID)가 friendId에 해당하는 친구 요청을 거절합니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "친구 요청이 거절되었습니다.")
})
@PostMapping("/reject/{friendId}")
SuccessResponse<FriendResponse> rejectFriend(
@Parameter(hidden = true) @MemberID String userId,
@Parameter(description = "거절할 친구 요청의 ID", required = true)
@PathVariable String friendId
);


@Operation(summary = "친구 삭제", description = "현재 사용자(@MemberID)가 friendId에 해당하는 친구 관계를 삭제(soft delete)합니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "친구 관계가 삭제되었습니다.")
})
@DeleteMapping("/{friendId}")
SuccessResponse<String> deleteFriend(
@Parameter(hidden = true) @MemberID String userId,
@Parameter(description = "삭제할 친구 관계의 ID", required = true)
@PathVariable String friendId
);


@Operation(summary = "보낸 친구 요청 목록 조회", description = "현재 사용자(@MemberID)가 보낸 친구 요청 목록(PENDING)을 조회합니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "보낸 친구 요청 목록이 조회되었습니다.")
})
@GetMapping("/sent")
SuccessResponse<FriendsResponse> getSentFriendRequests(
@Parameter(hidden = true) @MemberID String userId
);


@Operation(summary = "받은 친구 요청 목록 조회", description = "현재 사용자(@MemberID)가 받은 친구 요청 목록(PENDING)을 조회합니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "받은 친구 요청 목록이 조회되었습니다.")
})
@GetMapping("/received")
SuccessResponse<FriendsResponse> getReceivedFriendRequests(
@Parameter(hidden = true) @MemberID String userId
);


@Operation(summary = "친구 목록 조회", description = "현재 사용자(@MemberID)의 실제 친구 목록(ACCEPTED)을 조회합니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "친구 목록이 조회되었습니다.")
})
@GetMapping("/list")
SuccessResponse<FriendsResponse> getFriends(
@Parameter(hidden = true) @MemberID String userId
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.asyncgate.user_server.domain;

import lombok.Getter;

import java.util.UUID;

@Getter
public class Friend {
private final String id;
private final String userId1;
private final String userId2;
private final String requestedBy;
private FriendStatus status;

private Friend(String id, String userId1, String userId2, String requestedBy, FriendStatus status) {
this.id = id;
this.userId1 = userId1;
this.userId2 = userId2;
this.requestedBy = requestedBy;
this.status = status;
}

public static Friend create(String requestUserId, String toUserId) {
// 두 사용자 ID를 사전 순으로 정렬하여 저장
if (requestUserId.compareTo(toUserId) < 0) {
return new Friend(UUID.randomUUID().toString(), requestUserId, toUserId, requestUserId, FriendStatus.PENDING);
} else {
return new Friend(UUID.randomUUID().toString(), toUserId, requestUserId, requestUserId, FriendStatus.PENDING);
}
}

public static Friend of(String id, String userId1, String userId2, String requestedBy, FriendStatus status) {
return new Friend(id, userId1, userId2, requestedBy, status);
}

public void accept() {
this.status = FriendStatus.ACCEPTED;
}

public void reject() {
this.status = FriendStatus.REJECTED;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.asyncgate.user_server.domain;

public enum FriendStatus {
PENDING,
ACCEPTED,
REJECTED
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.asyncgate.user_server.dto.response;

import com.asyncgate.user_server.domain.Friend;
import com.asyncgate.user_server.domain.FriendStatus;

public record FriendResponse(String id, String userId1, String userId2, String requestedBy, FriendStatus status) {
public static FriendResponse from(final Friend friend) {
return new FriendResponse(
friend.getId(),
friend.getUserId1(),
friend.getUserId2(),
friend.getRequestedBy(),
friend.getStatus()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.asyncgate.user_server.dto.response;

import com.asyncgate.user_server.domain.Friend;

import java.util.List;

public record FriendsResponse(List<FriendResponse> friends) {
public static FriendsResponse from(final List<Friend> friendList) {
List<FriendResponse> responses = friendList.stream()
.map(FriendResponse::from)
.toList();
return new FriendsResponse(responses);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.asyncgate.user_server.dto.response;

import com.asyncgate.user_server.domain.Member;

import java.time.LocalDate;

public record MemberResponse(String email, String name, String nickname, String profileImgUrl, LocalDate birth) {

public static MemberResponse from(Member member) {
return new MemberResponse(member.getEmail(), member.getName(), member.getNickname(), member.getProfileImgUrl(), member.getBirth());
}

}
Loading