diff --git a/src/backend/user-server/build.gradle b/src/backend/user-server/build.gradle index 8cb5dd5f..058f2bc7 100644 --- a/src/backend/user-server/build.gradle +++ b/src/backend/user-server/build.gradle @@ -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' diff --git a/src/backend/user-server/src/main/java/com/asyncgate/user_server/config/QuerydslConfig.java b/src/backend/user-server/src/main/java/com/asyncgate/user_server/config/QuerydslConfig.java new file mode 100644 index 00000000..c3776ca2 --- /dev/null +++ b/src/backend/user-server/src/main/java/com/asyncgate/user_server/config/QuerydslConfig.java @@ -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); + } +} diff --git a/src/backend/user-server/src/main/java/com/asyncgate/user_server/controller/FriendController.java b/src/backend/user-server/src/main/java/com/asyncgate/user_server/controller/FriendController.java new file mode 100644 index 00000000..1247e96f --- /dev/null +++ b/src/backend/user-server/src/main/java/com/asyncgate/user_server/controller/FriendController.java @@ -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 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 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 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 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 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 getSentFriendRequests(final @MemberID String userId) { + List sent = friendUseCase.getSentFriendRequests(userId); + return SuccessResponse.ok( + FriendsResponse.from(sent) + ); + } + + /** + * 본인이 받은 친구 요청 목록 조회 (상태: PENDING) + * URL 예시: GET /friends/received + */ + @Override + @GetMapping("/received") + public SuccessResponse getReceivedFriendRequests(final @MemberID String userId) { + List received = friendUseCase.getReceivedFriendRequests(userId); + return SuccessResponse.ok( + FriendsResponse.from(received) + ); + } + + /** + * 본인의 실제 친구 목록 조회 (상태: ACCEPTED) + * URL 예시: GET /friends/list + */ + @Override + @GetMapping("/list") + public SuccessResponse getFriends(final @MemberID String userId) { + List friends = friendUseCase.getFriends(userId); + return SuccessResponse.ok( + FriendsResponse.from(friends) + ); + } +} diff --git a/src/backend/user-server/src/main/java/com/asyncgate/user_server/controller/docs/FriendControllerDocs.java b/src/backend/user-server/src/main/java/com/asyncgate/user_server/controller/docs/FriendControllerDocs.java new file mode 100644 index 00000000..e6b4bbae --- /dev/null +++ b/src/backend/user-server/src/main/java/com/asyncgate/user_server/controller/docs/FriendControllerDocs.java @@ -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 searchTarget( + @Parameter(description = "검색할 회원의 이메일", required = true) + @RequestParam String email + ); + + + @Operation(summary = "친구 요청", description = "현재 사용자(@MemberID)가 지정된 toUserId에게 친구 요청을 보냅니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "친구 요청이 생성되었습니다.") + }) + @PostMapping("/request/{toUserId}") + SuccessResponse 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 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 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 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 getSentFriendRequests( + @Parameter(hidden = true) @MemberID String userId + ); + + + @Operation(summary = "받은 친구 요청 목록 조회", description = "현재 사용자(@MemberID)가 받은 친구 요청 목록(PENDING)을 조회합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "받은 친구 요청 목록이 조회되었습니다.") + }) + @GetMapping("/received") + SuccessResponse getReceivedFriendRequests( + @Parameter(hidden = true) @MemberID String userId + ); + + + @Operation(summary = "친구 목록 조회", description = "현재 사용자(@MemberID)의 실제 친구 목록(ACCEPTED)을 조회합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "친구 목록이 조회되었습니다.") + }) + @GetMapping("/list") + SuccessResponse getFriends( + @Parameter(hidden = true) @MemberID String userId + ); +} diff --git a/src/backend/user-server/src/main/java/com/asyncgate/user_server/domain/Friend.java b/src/backend/user-server/src/main/java/com/asyncgate/user_server/domain/Friend.java new file mode 100644 index 00000000..e96f0364 --- /dev/null +++ b/src/backend/user-server/src/main/java/com/asyncgate/user_server/domain/Friend.java @@ -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; + } + +} \ No newline at end of file diff --git a/src/backend/user-server/src/main/java/com/asyncgate/user_server/domain/FriendStatus.java b/src/backend/user-server/src/main/java/com/asyncgate/user_server/domain/FriendStatus.java new file mode 100644 index 00000000..d88c05b6 --- /dev/null +++ b/src/backend/user-server/src/main/java/com/asyncgate/user_server/domain/FriendStatus.java @@ -0,0 +1,7 @@ +package com.asyncgate.user_server.domain; + +public enum FriendStatus { + PENDING, + ACCEPTED, + REJECTED +} diff --git a/src/backend/user-server/src/main/java/com/asyncgate/user_server/dto/response/FriendResponse.java b/src/backend/user-server/src/main/java/com/asyncgate/user_server/dto/response/FriendResponse.java new file mode 100644 index 00000000..d916f9e7 --- /dev/null +++ b/src/backend/user-server/src/main/java/com/asyncgate/user_server/dto/response/FriendResponse.java @@ -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() + ); + } +} diff --git a/src/backend/user-server/src/main/java/com/asyncgate/user_server/dto/response/FriendsResponse.java b/src/backend/user-server/src/main/java/com/asyncgate/user_server/dto/response/FriendsResponse.java new file mode 100644 index 00000000..39ca7f9b --- /dev/null +++ b/src/backend/user-server/src/main/java/com/asyncgate/user_server/dto/response/FriendsResponse.java @@ -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 friends) { + public static FriendsResponse from(final List friendList) { + List responses = friendList.stream() + .map(FriendResponse::from) + .toList(); + return new FriendsResponse(responses); + } +} diff --git a/src/backend/user-server/src/main/java/com/asyncgate/user_server/dto/response/MemberResponse.java b/src/backend/user-server/src/main/java/com/asyncgate/user_server/dto/response/MemberResponse.java new file mode 100644 index 00000000..dc3864bf --- /dev/null +++ b/src/backend/user-server/src/main/java/com/asyncgate/user_server/dto/response/MemberResponse.java @@ -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()); + } + +} diff --git a/src/backend/user-server/src/main/java/com/asyncgate/user_server/entity/FriendEntity.java b/src/backend/user-server/src/main/java/com/asyncgate/user_server/entity/FriendEntity.java new file mode 100644 index 00000000..21e5c995 --- /dev/null +++ b/src/backend/user-server/src/main/java/com/asyncgate/user_server/entity/FriendEntity.java @@ -0,0 +1,45 @@ +package com.asyncgate.user_server.entity; + +import com.asyncgate.user_server.domain.FriendStatus; +import com.asyncgate.user_server.entity.common.BaseEntity; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "friend", indexes = { + @Index(name = "idx_user1_user2", columnList = "userId1, userId2"), + @Index(name = "idx_user2_user1", columnList = "userId2, userId1") +}) +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class FriendEntity extends BaseEntity { + + @Id + private String id; + + @Column(name = "userId1", nullable = false) + private String userId1; + + @Column(name = "userId2", nullable = false) + private String userId2; + + @Column(name = "requested_by", nullable = false) + private String requestedBy; + + @Enumerated(EnumType.STRING) + @Column(name = "status", nullable = false) + private FriendStatus status; + + @Builder + public FriendEntity(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; + } +} + diff --git a/src/backend/user-server/src/main/java/com/asyncgate/user_server/exception/FailType.java b/src/backend/user-server/src/main/java/com/asyncgate/user_server/exception/FailType.java index 2ef02043..a109780f 100644 --- a/src/backend/user-server/src/main/java/com/asyncgate/user_server/exception/FailType.java +++ b/src/backend/user-server/src/main/java/com/asyncgate/user_server/exception/FailType.java @@ -48,7 +48,12 @@ public enum FailType { ACCESS_DENIED(HttpStatus.FORBIDDEN, "Access_40300", "접근 권한이 없습니다."), NOT_LOGIN_USER(HttpStatus.FORBIDDEN, "Access_40301", "로그인하지 않은 사용자입니다."), ARGUMENT_BAD_REQUEST(HttpStatus.BAD_REQUEST, "Argument_4001", "입력된 값이 잘못되었습니다."), - ; + + + // Friend + FRIEND_NOT_FOUND(HttpStatus.NOT_FOUND, "Friend_4001", "친구 정보가 존재하지 않습니다."), + UNAUTHORIZED_ACCESS(HttpStatus.FORBIDDEN, "Friend_40300", "현재 사용자는 해당 친구 관계에 대한 권한이 없습니다."), + UNAUTHORIZED_ACTION(HttpStatus.FORBIDDEN, "Friend_40301", "본인의 친구 요청에 대해서는 해당 작업을 수행할 수 없습니다.");; private final HttpStatus status; private final String errorCode; diff --git a/src/backend/user-server/src/main/java/com/asyncgate/user_server/repository/FriendJpaRepository.java b/src/backend/user-server/src/main/java/com/asyncgate/user_server/repository/FriendJpaRepository.java new file mode 100644 index 00000000..3ef90cb3 --- /dev/null +++ b/src/backend/user-server/src/main/java/com/asyncgate/user_server/repository/FriendJpaRepository.java @@ -0,0 +1,11 @@ +package com.asyncgate.user_server.repository; + +import com.asyncgate.user_server.entity.FriendEntity; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface FriendJpaRepository extends JpaRepository { + + Optional findByIdAndDeletedFalse(String id); +} diff --git a/src/backend/user-server/src/main/java/com/asyncgate/user_server/repository/FriendQueryDslRepository.java b/src/backend/user-server/src/main/java/com/asyncgate/user_server/repository/FriendQueryDslRepository.java new file mode 100644 index 00000000..3c2eb2cc --- /dev/null +++ b/src/backend/user-server/src/main/java/com/asyncgate/user_server/repository/FriendQueryDslRepository.java @@ -0,0 +1,60 @@ +package com.asyncgate.user_server.repository; + +import com.asyncgate.user_server.domain.FriendStatus; +import com.asyncgate.user_server.entity.FriendEntity; +import com.asyncgate.user_server.entity.QFriendEntity; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +@RequiredArgsConstructor +public class FriendQueryDslRepository { + + private final JPAQueryFactory jpaQueryFactory; + private final QFriendEntity friendEntity = QFriendEntity.friendEntity; + + /** + * 본인이 보낸 친구 요청: 요청자(requestedBy)가 userId인 경우, 상태(PENDING)와 soft delete 조건 적용 + */ + public List findSentFriendRequests(final String requestedUserId) { + return jpaQueryFactory.selectFrom(friendEntity) + .where( + friendEntity.requestedBy.eq(requestedUserId), + friendEntity.status.eq(FriendStatus.PENDING), + friendEntity.deleted.eq(false) + ) + .fetch(); + } + + /** + * 본인이 받은 친구 요청: 친구 관계에 포함되면서 요청자(requestedBy)가 userId가 아닌 경우 (상태=PENDING) + */ + public List findReceivedFriendRequests(final String userId) { + return jpaQueryFactory.selectFrom(friendEntity) + .where( + friendEntity.status.eq(FriendStatus.PENDING), + friendEntity.deleted.eq(false), + friendEntity.userId1.eq(userId) + .or(friendEntity.userId2.eq(userId)), + friendEntity.requestedBy.ne(userId) + ) + .fetch(); + } + + /** + * 본인의 실제 친구: 친구 관계에 포함되고 상태가 ACCEPTED인 경우 (soft delete 조건 포함) + */ + public List findFriendsByUserId(final String userId) { + return jpaQueryFactory.selectFrom(friendEntity) + .where( + friendEntity.status.eq(FriendStatus.ACCEPTED), + friendEntity.deleted.eq(false), + friendEntity.userId1.eq(userId) + .or(friendEntity.userId2.eq(userId)) + ) + .fetch(); + } +} diff --git a/src/backend/user-server/src/main/java/com/asyncgate/user_server/repository/FriendRepository.java b/src/backend/user-server/src/main/java/com/asyncgate/user_server/repository/FriendRepository.java new file mode 100644 index 00000000..3021afee --- /dev/null +++ b/src/backend/user-server/src/main/java/com/asyncgate/user_server/repository/FriendRepository.java @@ -0,0 +1,19 @@ +package com.asyncgate.user_server.repository; + +import com.asyncgate.user_server.domain.Friend; + +import java.util.List; + +public interface FriendRepository { + Friend findById(String id); + + Friend save(Friend entity); + + void deleteById(String friendId); + + List findSentFriendRequests(String requestedUserId); + + List findReceivedFriendRequests(String userId); + + List findFriendsByUserId(String userId); +} diff --git a/src/backend/user-server/src/main/java/com/asyncgate/user_server/repository/FriendRepositoryImpl.java b/src/backend/user-server/src/main/java/com/asyncgate/user_server/repository/FriendRepositoryImpl.java new file mode 100644 index 00000000..ed2bdd57 --- /dev/null +++ b/src/backend/user-server/src/main/java/com/asyncgate/user_server/repository/FriendRepositoryImpl.java @@ -0,0 +1,67 @@ +package com.asyncgate.user_server.repository; + +import com.asyncgate.user_server.domain.Friend; +import com.asyncgate.user_server.entity.common.BaseEntity; +import com.asyncgate.user_server.exception.FailType; +import com.asyncgate.user_server.exception.UserServerException; +import com.asyncgate.user_server.support.utility.DomainUtil; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +@RequiredArgsConstructor +public class FriendRepositoryImpl implements FriendRepository { + + private final FriendJpaRepository friendJpaRepository; + private final FriendQueryDslRepository queryDslRepository; + + @Override + public Friend findById(final String id) { + return DomainUtil.FriendMapper.toDomain( + friendJpaRepository.findByIdAndDeletedFalse(id) + .orElseThrow( + () -> new UserServerException(FailType.FRIEND_NOT_FOUND) + ) + ); + } + + @Override + public Friend save(final Friend domain) { + friendJpaRepository.save(DomainUtil.FriendMapper.toEntity(domain)); + return domain; + } + + @Override + public void deleteById(final String friendId) { + friendJpaRepository.findById(friendId) + .ifPresent( + BaseEntity::deactivate + ); + } + + @Override + public List findSentFriendRequests(final String requestedUserId) { + return queryDslRepository.findSentFriendRequests(requestedUserId) + .stream().map( + DomainUtil.FriendMapper::toDomain + ).toList(); + } + + @Override + public List findReceivedFriendRequests(final String userId) { + return queryDslRepository.findReceivedFriendRequests(userId) + .stream().map( + DomainUtil.FriendMapper::toDomain + ).toList(); + } + + @Override + public List findFriendsByUserId(final String userId) { + return queryDslRepository.findFriendsByUserId(userId) + .stream().map( + DomainUtil.FriendMapper::toDomain + ).toList(); + } +} diff --git a/src/backend/user-server/src/main/java/com/asyncgate/user_server/service/FriendService.java b/src/backend/user-server/src/main/java/com/asyncgate/user_server/service/FriendService.java new file mode 100644 index 00000000..9ab157c7 --- /dev/null +++ b/src/backend/user-server/src/main/java/com/asyncgate/user_server/service/FriendService.java @@ -0,0 +1,95 @@ +package com.asyncgate.user_server.service; + +import com.asyncgate.user_server.domain.Friend; +import com.asyncgate.user_server.domain.Member; +import com.asyncgate.user_server.exception.FailType; +import com.asyncgate.user_server.exception.UserServerException; +import com.asyncgate.user_server.repository.FriendRepository; +import com.asyncgate.user_server.repository.MemberRepository; +import com.asyncgate.user_server.usecase.FriendUseCase; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class FriendService implements FriendUseCase { + + private final FriendRepository friendRepository; + private final MemberRepository memberRepository; + + @Override + public Member getByEmail(final String email) { + return memberRepository.findByEmail(email) + .orElseThrow(() -> new UserServerException(FailType.MEMBER_NOT_FOUND)); + } + + @Override + @Transactional + public Friend registerFriend(final String requestUserId, final String toUserId) { + Friend friend = Friend.create(requestUserId, toUserId); + friendRepository.save(friend); + return friend; + } + + @Override + @Transactional + public Friend acceptFriend(final String userId, final String friendId) { + Friend friend = friendRepository.findById(friendId); + validEditPermission(userId, friend); + friend.accept(); + friendRepository.save(friend); + return friend; + } + + private void validEditPermission(String userId, Friend friend) { + validDeletePermission(userId, friend); + // 현재 사용자가 요청을 보낸 사람(requestedBy)인 경우, 수락할 수 없음 + if (userId.equals(friend.getRequestedBy())) { + throw new UserServerException(FailType.UNAUTHORIZED_ACTION); + } + } + + // 권한 체크: 현재 사용자가 friend 관계에 포함되어 있는지 확인 + private void validDeletePermission(String userId, Friend friend) { + if (!(userId.equals(friend.getUserId1()) || userId.equals(friend.getUserId2()))) { + throw new UserServerException(FailType.UNAUTHORIZED_ACCESS); + } + } + + @Override + @Transactional + public Friend rejectFriend(final String userId, final String friendId) { + Friend friend = friendRepository.findById(friendId); + validEditPermission(userId, friend); + friend.reject(); + friendRepository.save(friend); + return friend; + } + + @Override + @Transactional + public void deleteFriend(final String userId, final String friendId) { + Friend friend = friendRepository.findById(friendId); + validDeletePermission(userId, friend); + friendRepository.deleteById(friendId); + } + + @Override + public List getSentFriendRequests(final String userId) { + return friendRepository.findSentFriendRequests(userId); + } + + @Override + public List getReceivedFriendRequests(final String userId) { + return friendRepository.findReceivedFriendRequests(userId); + } + + @Override + public List getFriends(final String userId) { + return friendRepository.findFriendsByUserId(userId); + } +} diff --git a/src/backend/user-server/src/main/java/com/asyncgate/user_server/support/utility/DomainUtil.java b/src/backend/user-server/src/main/java/com/asyncgate/user_server/support/utility/DomainUtil.java index 1faa5d0a..2a47a6cb 100644 --- a/src/backend/user-server/src/main/java/com/asyncgate/user_server/support/utility/DomainUtil.java +++ b/src/backend/user-server/src/main/java/com/asyncgate/user_server/support/utility/DomainUtil.java @@ -1,8 +1,10 @@ package com.asyncgate.user_server.support.utility; -import com.asyncgate.user_server.domain.Member; import com.asyncgate.user_server.domain.AuthenticationCode; +import com.asyncgate.user_server.domain.Friend; +import com.asyncgate.user_server.domain.Member; import com.asyncgate.user_server.domain.TemporaryMember; +import com.asyncgate.user_server.entity.FriendEntity; import com.asyncgate.user_server.entity.MemberEntity; import com.asyncgate.user_server.entity.redis.AuthenticationCodeEntity; import com.asyncgate.user_server.entity.redis.TemporaryMemberEntity; @@ -75,4 +77,26 @@ public static AuthenticationCode toDomain(final AuthenticationCodeEntity entity) .build(); } } + + public static class FriendMapper { + public static FriendEntity toEntity(final Friend friend) { + return FriendEntity.builder() + .id(friend.getId()) + .userId1(friend.getUserId1()) + .userId2(friend.getUserId2()) + .requestedBy(friend.getRequestedBy()) + .status(friend.getStatus()) + .build(); + } + + public static Friend toDomain(final FriendEntity entity) { + return Friend.of( + entity.getId(), + entity.getUserId1(), + entity.getUserId2(), + entity.getRequestedBy(), + entity.getStatus() + ); + } + } } diff --git a/src/backend/user-server/src/main/java/com/asyncgate/user_server/usecase/FriendUseCase.java b/src/backend/user-server/src/main/java/com/asyncgate/user_server/usecase/FriendUseCase.java new file mode 100644 index 00000000..347a349e --- /dev/null +++ b/src/backend/user-server/src/main/java/com/asyncgate/user_server/usecase/FriendUseCase.java @@ -0,0 +1,25 @@ +package com.asyncgate.user_server.usecase; + +import com.asyncgate.user_server.domain.Friend; +import com.asyncgate.user_server.domain.Member; + +import java.util.List; + +public interface FriendUseCase { + Member getByEmail(String email); + + Friend registerFriend(String requestUserId, String toUserId); + + Friend acceptFriend(String userId, String friendId); + + Friend rejectFriend(String userId, String friendId); + + void deleteFriend(String userId, String friendId); + + List getFriends(String userId); + + List getSentFriendRequests(String userId); + + List getReceivedFriendRequests(String userId); + +} \ No newline at end of file