From c79b3f8f2e4cce90ed3118eddceb81a29544fa3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=84=B8=EC=A7=84?= Date: Fri, 11 Oct 2024 13:10:04 +0900 Subject: [PATCH 01/46] =?UTF-8?q?feat:=20=EC=B9=9C=EA=B5=ACapi=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=EA=B5=AC=ED=98=84=20(=EC=9A=94=EC=B2=AD=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=20404=20=EB=9C=B8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../splanet/core/exception/ErrorCode.java | 7 +- .../splanet/friend/controller/FriendApi.java | 42 ++++++++++ .../friend/controller/FriendController.java | 40 ++++++++++ .../splanet/friend/dto/FriendResponse.java | 4 + .../splanet/splanet/friend/entity/Friend.java | 11 ++- .../friend/repository/FriendRepository.java | 6 ++ .../splanet/friend/service/FriendService.java | 53 +++++++++++++ .../controller/FriendRequestApi.java | 69 ++++++++++++++++ .../controller/FriendRequestController.java | 55 +++++++++++++ .../dto/FriendRequestRequest.java | 4 + .../dto/FriendRequestResponse.java | 4 + .../friendRequest/entity/FriendRequest.java | 19 ++++- .../repository/FriendRequestRepository.java | 10 +++ .../service/FriendRequestService.java | 78 +++++++++++++++++++ 14 files changed, 393 insertions(+), 9 deletions(-) create mode 100644 src/main/java/com/splanet/splanet/friend/controller/FriendApi.java create mode 100644 src/main/java/com/splanet/splanet/friend/controller/FriendController.java create mode 100644 src/main/java/com/splanet/splanet/friend/dto/FriendResponse.java create mode 100644 src/main/java/com/splanet/splanet/friend/service/FriendService.java create mode 100644 src/main/java/com/splanet/splanet/friendRequest/controller/FriendRequestApi.java create mode 100644 src/main/java/com/splanet/splanet/friendRequest/controller/FriendRequestController.java create mode 100644 src/main/java/com/splanet/splanet/friendRequest/dto/FriendRequestRequest.java create mode 100644 src/main/java/com/splanet/splanet/friendRequest/dto/FriendRequestResponse.java create mode 100644 src/main/java/com/splanet/splanet/friendRequest/service/FriendRequestService.java diff --git a/src/main/java/com/splanet/splanet/core/exception/ErrorCode.java b/src/main/java/com/splanet/splanet/core/exception/ErrorCode.java index a38944ad..1707b2fa 100644 --- a/src/main/java/com/splanet/splanet/core/exception/ErrorCode.java +++ b/src/main/java/com/splanet/splanet/core/exception/ErrorCode.java @@ -34,7 +34,12 @@ public enum ErrorCode { TEAM_MEMBER_NOT_FOUND("해당 유저는 팀에 속해 있지 않습니다.", HttpStatus.NOT_FOUND), INVITATION_NOT_FOUND("초대를 찾을 수 없습니다.", HttpStatus.NOT_FOUND), INVITATION_ALREADY_PROCESSED("초대가 이미 처리되었습니다.", HttpStatus.BAD_REQUEST), - USER_ALREADY_IN_TEAM("해당 유저는 이미 팀에 속해 있습니다.", HttpStatus.BAD_REQUEST); + USER_ALREADY_IN_TEAM("해당 유저는 이미 팀에 속해 있습니다.", HttpStatus.BAD_REQUEST), + + + // friend + FRIEND_REQUEST_NOT_FOUND("받은 친구 요청이 없습니다.", HttpStatus.NOT_FOUND); + private final String message; private final HttpStatus status; diff --git a/src/main/java/com/splanet/splanet/friend/controller/FriendApi.java b/src/main/java/com/splanet/splanet/friend/controller/FriendApi.java new file mode 100644 index 00000000..5255ab5e --- /dev/null +++ b/src/main/java/com/splanet/splanet/friend/controller/FriendApi.java @@ -0,0 +1,42 @@ +package com.splanet.splanet.friend.controller; + +import com.splanet.splanet.friend.dto.FriendResponse; +import com.splanet.splanet.subscription.dto.SubscriptionResponse; +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 io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; + +import java.util.List; + +@RequestMapping("/api/friends") +@Tag(name = "Friend", description = "친구 관련 API") +public interface FriendApi { + + @GetMapping + @Operation(summary = "친구 목록 조회", description = "사용자의 친구 목록을 조회합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "친구 목록이 성공적으로 조회되었습니다."), + @ApiResponse(responseCode = "401", description = "인증되지 않은 사용자입니다.") + }) + ResponseEntity> getFriends( + @Parameter(description = "JWT 인증으로 전달된 사용자 ID", required = true) @AuthenticationPrincipal Long userId); + + @GetMapping("/{friend_id}/plans") + @Operation(summary = "친구 플랜 조회", description = "친구의 플랜 목록을 조회합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "친구의 공개된 계획이 성공적으로 조회되었습니다."), + @ApiResponse(responseCode = "400", description = "잘못된 요청입니다 (유효하지 않은 친구 ID)."), + @ApiResponse(responseCode = "401", description = "인증되지 않은 사용자입니다."), + @ApiResponse(responseCode = "404", description = "친구를 찾을 수 없습니다.") + }) + ResponseEntity getFriendPlan( + @Parameter(description = "JWT 인증으로 전달된 친구 ID", required = true) @PathVariable("friend_id") Long friendId, + @Parameter(description = "JWT 인증으로 전달된 사용자 ID", required = true) @AuthenticationPrincipal Long userId); +} \ No newline at end of file diff --git a/src/main/java/com/splanet/splanet/friend/controller/FriendController.java b/src/main/java/com/splanet/splanet/friend/controller/FriendController.java new file mode 100644 index 00000000..46be1a4d --- /dev/null +++ b/src/main/java/com/splanet/splanet/friend/controller/FriendController.java @@ -0,0 +1,40 @@ +package com.splanet.splanet.friend.controller; + +import com.splanet.splanet.core.exception.BusinessException; +import com.splanet.splanet.core.exception.ErrorCode; +import com.splanet.splanet.friend.dto.FriendResponse; +import com.splanet.splanet.friend.service.FriendService; +import com.splanet.splanet.user.entity.User; +import com.splanet.splanet.user.repository.UserRepository; +import com.splanet.splanet.user.service.UserService; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +public class FriendController implements FriendApi { + + private final FriendService friendService; + private final UserRepository userRepository; + + public FriendController(FriendService friendService, UserRepository userRepository) { + this.friendService = friendService; + this.userRepository = userRepository; + } + + @Override + public ResponseEntity> getFriends(Long userId) { + List friends = friendService.getFriends(userId); + return ResponseEntity.ok(friends); + } + + @Override + public ResponseEntity getFriendPlan(Long friendId, Long userId) { + // 친구의 닉네임을 반환하는 임시 구현 + User friendUser = userRepository.findById(friendId) + .orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND)); + + return ResponseEntity.ok(friendUser.getNickname()); + } +} diff --git a/src/main/java/com/splanet/splanet/friend/dto/FriendResponse.java b/src/main/java/com/splanet/splanet/friend/dto/FriendResponse.java new file mode 100644 index 00000000..1ba32453 --- /dev/null +++ b/src/main/java/com/splanet/splanet/friend/dto/FriendResponse.java @@ -0,0 +1,4 @@ +package com.splanet.splanet.friend.dto; + +public record FriendResponse(Long userId, String nickname, String profileImage) { +} \ No newline at end of file diff --git a/src/main/java/com/splanet/splanet/friend/entity/Friend.java b/src/main/java/com/splanet/splanet/friend/entity/Friend.java index 0036adbf..8be6e070 100644 --- a/src/main/java/com/splanet/splanet/friend/entity/Friend.java +++ b/src/main/java/com/splanet/splanet/friend/entity/Friend.java @@ -1,6 +1,7 @@ package com.splanet.splanet.friend.entity; import com.splanet.splanet.core.BaseEntity; +import com.splanet.splanet.user.entity.User; import jakarta.persistence.*; import lombok.*; import lombok.experimental.SuperBuilder; @@ -13,9 +14,11 @@ @Entity public class Friend extends BaseEntity { - @Column(name = "user_id", nullable = false) - private Long userId; + @ManyToOne + @JoinColumn(name = "user_id", nullable = false) + private User user; - @Column(name = "friend_id", nullable = false) - private Long friendId; + @ManyToOne + @JoinColumn(name = "friend_id", nullable = false) + private User friend; } \ No newline at end of file diff --git a/src/main/java/com/splanet/splanet/friend/repository/FriendRepository.java b/src/main/java/com/splanet/splanet/friend/repository/FriendRepository.java index 48b5fc00..08ee4367 100644 --- a/src/main/java/com/splanet/splanet/friend/repository/FriendRepository.java +++ b/src/main/java/com/splanet/splanet/friend/repository/FriendRepository.java @@ -4,6 +4,12 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; +import java.util.List; +import java.util.Optional; + @Repository public interface FriendRepository extends JpaRepository { + List findByUserId(Long userId); + List findByFriendId(Long friendId); + Optional findByUserIdAndFriendId(Long userId, Long friendId); } \ No newline at end of file diff --git a/src/main/java/com/splanet/splanet/friend/service/FriendService.java b/src/main/java/com/splanet/splanet/friend/service/FriendService.java new file mode 100644 index 00000000..22a41f33 --- /dev/null +++ b/src/main/java/com/splanet/splanet/friend/service/FriendService.java @@ -0,0 +1,53 @@ +package com.splanet.splanet.friend.service; + +import com.splanet.splanet.core.exception.BusinessException; +import com.splanet.splanet.core.exception.ErrorCode; +import com.splanet.splanet.friend.dto.FriendResponse; +import com.splanet.splanet.friend.entity.Friend; +import com.splanet.splanet.friend.repository.FriendRepository; +import com.splanet.splanet.user.entity.User; +import com.splanet.splanet.user.repository.UserRepository; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +public class FriendService { + + private final FriendRepository friendRepository; + private final UserRepository userRepository; + + public FriendService(FriendRepository friendRepository, UserRepository userRepository) { + this.friendRepository = friendRepository; + this.userRepository = userRepository; + } + + // 친구 목록 조회 + public List getFriends(Long userId) { + List friends = friendRepository.findByUserId(userId); + + // Friend 엔티티를 FriendResponse DTO로 변환 + return friends.stream() + .map(friend -> { + User friendUser = friend.getFriend(); + return new FriendResponse( + friendUser.getKakaoId(), + friendUser.getNickname(), + friendUser.getProfileImage() + ); + }) + .collect(Collectors.toList()); + } + + // 친구의 플랜 조회 (plan api 머지 전이라 일단 친구 닉네임 반환으로 임시설정) + public ResponseEntity getFriendPlan(Long friendId, Long userId) { + Friend friendRelationship = friendRepository.findByUserIdAndFriendId(userId, friendId) + .orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND)); + + User friend = friendRelationship.getFriend(); + + return ResponseEntity.ok(friend.getNickname()); + } +} \ No newline at end of file diff --git a/src/main/java/com/splanet/splanet/friendRequest/controller/FriendRequestApi.java b/src/main/java/com/splanet/splanet/friendRequest/controller/FriendRequestApi.java new file mode 100644 index 00000000..3e0c8dfa --- /dev/null +++ b/src/main/java/com/splanet/splanet/friendRequest/controller/FriendRequestApi.java @@ -0,0 +1,69 @@ +package com.splanet.splanet.friendRequest.controller; + +import com.splanet.splanet.friendRequest.dto.FriendRequestRequest; +import com.splanet.splanet.friendRequest.dto.FriendRequestResponse; +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 io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RequestMapping("/api/friends/requests") +@Tag(name = "FriendRequest", description = "친구 요청 관련 API") +public interface FriendRequestApi { + + @PostMapping + @Operation(summary = "친구 요청", description = "특정 사용자에게 친구 요청을 보냅니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "친구 요청이 성공적으로 전송되었습니다."), + @ApiResponse(responseCode = "400", description = "잘못된 요청입니다 (유효하지 않은 유저 ID)."), + @ApiResponse(responseCode = "401", description = "인증되지 않은 사용자입니다.") + }) + ResponseEntity sendFriendRequest( + @Parameter(description = "친구 요청을 보낼 사용자 ID", required = true) @RequestBody FriendRequestRequest request); + + @PostMapping("/{request_id}/accept") + @Operation(summary = "친구 요청 수락", description = "친구 요청 수락") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "친구 요청 성공적으로 수락되었습니다."), + @ApiResponse(responseCode = "400", description = "잘못된 요청입니다 (유효하지 않은 유저 ID)."), + @ApiResponse(responseCode = "401", description = "인증되지 않은 사용자입니다."), + @ApiResponse(responseCode = "404", description = "친구 요청을 찾을 수 없습니다.") + }) + ResponseEntity acceptFriendRequest( + @Parameter(description = "친구 요청 ID", required = true) @PathVariable Long requestId); + + @PostMapping("/{request_id}/reject") + @Operation(summary = "친구 요청 거절", description = "친구 요청 거절") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "친구 요청이 성공적으로 거절되었습니다."), + @ApiResponse(responseCode = "401", description = "인증되지 않은 사용자입니다."), + @ApiResponse(responseCode = "404", description = "친구 요청을 찾을 수 없습니다."), + @ApiResponse(responseCode = "404", description = "친구 요청을 찾을 수 없습니다.") + }) + ResponseEntity rejectFriendRequest( + @Parameter(description = "친구 요청 ID", required = true) @PathVariable Long requestId); + + @GetMapping("/received") + @Operation(summary = "친구 요청 목록 조회 (받은 요청)", description = "사용자가 받은 친구 요청 목록을 조회합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "받은 친구 요청 목록이 성공적으로 조회되었습니다."), + @ApiResponse(responseCode = "401", description = "인증되지 않은 사용자입니다.") + }) + ResponseEntity> getReceivedRequests( + @Parameter(description = "JWT 인증으로 전달된 사용자 ID", required = true) @AuthenticationPrincipal Long userId); + + @GetMapping("/sent") + @Operation(summary = "친구 요청 목록 조회 (보낸 요청)", description = "사용자가 보낸 친구 요청 목록을 조회합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "보낸 친구 요청 목록이 성공적으로 조회되었습니다."), + @ApiResponse(responseCode = "401", description = "인증되지 않은 사용자입니다.") + }) + ResponseEntity> getSentRequests( + @Parameter(description = "JWT 인증으로 전달된 사용자 ID", required = true) @AuthenticationPrincipal Long userId); +} \ No newline at end of file diff --git a/src/main/java/com/splanet/splanet/friendRequest/controller/FriendRequestController.java b/src/main/java/com/splanet/splanet/friendRequest/controller/FriendRequestController.java new file mode 100644 index 00000000..be44534b --- /dev/null +++ b/src/main/java/com/splanet/splanet/friendRequest/controller/FriendRequestController.java @@ -0,0 +1,55 @@ +package com.splanet.splanet.friendRequest.controller; + +import com.splanet.splanet.friendRequest.dto.FriendRequestRequest; +import com.splanet.splanet.friendRequest.dto.FriendRequestResponse; +import com.splanet.splanet.friendRequest.service.FriendRequestService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +public class FriendRequestController implements FriendRequestApi{ + + private final FriendRequestService friendRequestService; + + public FriendRequestController(FriendRequestService friendRequestService) { + this.friendRequestService = friendRequestService; + } + + // 친구 요청 전송 + @Override + public ResponseEntity sendFriendRequest(@RequestBody FriendRequestRequest request) { + friendRequestService.sendFriendRequest(request.requesterId(), request.receiverId()); + return ResponseEntity.ok(new FriendRequestResponse("친구 요청이 성공적으로 전송되었습니다.")); + } + + // 친구 요청 수락 + @Override + public ResponseEntity acceptFriendRequest(@PathVariable Long requestId) { + friendRequestService.acceptFriendRequest(requestId); + return ResponseEntity.ok(new FriendRequestResponse("친구 요청이 성공적으로 수락되었습니다.")); + } + + // 친구 요청 거절 + @Override + public ResponseEntity rejectFriendRequest(@PathVariable Long requestId) { + friendRequestService.rejectFriendRequest(requestId); + return ResponseEntity.ok(new FriendRequestResponse("친구 요청이 성공적으로 거절되었습니다.")); + } + + // 친구 요청 목록 조회(받은 요청) + @Override + public ResponseEntity> getReceivedRequests(@RequestParam Long userId) { + List requests = friendRequestService.getReceivedFriendRequests(userId); + return ResponseEntity.ok(requests); + } + + // 친구 요청 목록 조회(보낸 요청) + @Override + public ResponseEntity> getSentRequests(@RequestParam Long userId) { + List requests = friendRequestService.getSentFriendRequests(userId); + return ResponseEntity.ok(requests); + } +} \ No newline at end of file diff --git a/src/main/java/com/splanet/splanet/friendRequest/dto/FriendRequestRequest.java b/src/main/java/com/splanet/splanet/friendRequest/dto/FriendRequestRequest.java new file mode 100644 index 00000000..0574fd31 --- /dev/null +++ b/src/main/java/com/splanet/splanet/friendRequest/dto/FriendRequestRequest.java @@ -0,0 +1,4 @@ +package com.splanet.splanet.friendRequest.dto; + +public record FriendRequestRequest(Long requesterId, Long receiverId) { +} \ No newline at end of file diff --git a/src/main/java/com/splanet/splanet/friendRequest/dto/FriendRequestResponse.java b/src/main/java/com/splanet/splanet/friendRequest/dto/FriendRequestResponse.java new file mode 100644 index 00000000..c2bcc2ee --- /dev/null +++ b/src/main/java/com/splanet/splanet/friendRequest/dto/FriendRequestResponse.java @@ -0,0 +1,4 @@ +package com.splanet.splanet.friendRequest.dto; + +public record FriendRequestResponse(String message) { +} diff --git a/src/main/java/com/splanet/splanet/friendRequest/entity/FriendRequest.java b/src/main/java/com/splanet/splanet/friendRequest/entity/FriendRequest.java index c89544ca..31e48aaa 100644 --- a/src/main/java/com/splanet/splanet/friendRequest/entity/FriendRequest.java +++ b/src/main/java/com/splanet/splanet/friendRequest/entity/FriendRequest.java @@ -1,6 +1,7 @@ package com.splanet.splanet.friendRequest.entity; import com.splanet.splanet.core.BaseEntity; +import com.splanet.splanet.user.entity.User; import jakarta.persistence.*; import lombok.*; import lombok.experimental.SuperBuilder; @@ -13,11 +14,13 @@ @Entity public class FriendRequest extends BaseEntity { - @Column(name = "requester_id", nullable = false) - private Long requesterId; + @ManyToOne + @JoinColumn(name = "requester_id", nullable = false) + private User requester; - @Column(name = "receiver_id", nullable = false) - private Long receiverId; + @ManyToOne + @JoinColumn(name = "receiver_id", nullable = false) + private User receiver; @Enumerated(EnumType.STRING) @Column(name = "status") @@ -34,4 +37,12 @@ public enum Status { ACCEPTED, REJECTED } + + public Long getRequesterId() { + return requester != null ? requester.getId() : null; + } + + public Long getReceiverId() { + return receiver != null ? receiver.getId() : null; + } } \ No newline at end of file diff --git a/src/main/java/com/splanet/splanet/friendRequest/repository/FriendRequestRepository.java b/src/main/java/com/splanet/splanet/friendRequest/repository/FriendRequestRepository.java index 26560222..ab73a070 100644 --- a/src/main/java/com/splanet/splanet/friendRequest/repository/FriendRequestRepository.java +++ b/src/main/java/com/splanet/splanet/friendRequest/repository/FriendRequestRepository.java @@ -2,8 +2,18 @@ import com.splanet.splanet.friendRequest.entity.FriendRequest; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; +import org.w3c.dom.stylesheets.LinkStyle; + +import java.util.List; @Repository public interface FriendRequestRepository extends JpaRepository { + @Query("SELECT fr FROM FriendRequest fr WHERE fr.receiver.id = :userId") + List findByReceiverId(@Param("userId") Long userId); + + @Query("SELECT fr FROM FriendRequest fr WHERE fr.requester.id = :userId") + List findByRequesterId(@Param("userId") Long userId); } \ No newline at end of file diff --git a/src/main/java/com/splanet/splanet/friendRequest/service/FriendRequestService.java b/src/main/java/com/splanet/splanet/friendRequest/service/FriendRequestService.java new file mode 100644 index 00000000..f212b17f --- /dev/null +++ b/src/main/java/com/splanet/splanet/friendRequest/service/FriendRequestService.java @@ -0,0 +1,78 @@ +package com.splanet.splanet.friendRequest.service; + +import com.splanet.splanet.core.exception.BusinessException; +import com.splanet.splanet.core.exception.ErrorCode; +import com.splanet.splanet.friendRequest.dto.FriendRequestResponse; +import com.splanet.splanet.friendRequest.entity.FriendRequest; +import com.splanet.splanet.friendRequest.repository.FriendRequestRepository; +import com.splanet.splanet.user.entity.User; +import com.splanet.splanet.user.repository.UserRepository; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +public class FriendRequestService { + + private final FriendRequestRepository friendRequestRepository; + private final UserRepository userRepository; + + public FriendRequestService(FriendRequestRepository friendRequestRepository, UserRepository userRepository) { + this.friendRequestRepository = friendRequestRepository; + this.userRepository = userRepository; + } + + // 친구 요청 전송 + public void sendFriendRequest(Long requesterId, Long receiverId) { + User requester = userRepository.findById(requesterId) + .orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND)); + + User receiver = userRepository.findById(receiverId) + .orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND)); + + // FriendRequest 객체를 생성할 때 User 객체를 사용 + FriendRequest friendRequest = new FriendRequest(requester, receiver, FriendRequest.Status.PENDING); + friendRequestRepository.save(friendRequest); + } + + // 친구 요청 수락 + public void acceptFriendRequest(Long requestId) { + FriendRequest friendRequest = friendRequestRepository.findById(requestId) + .orElseThrow(() -> new BusinessException(ErrorCode.FRIEND_REQUEST_NOT_FOUND)); + + // 요청 상태를 ACCEPTED로 변경 + friendRequest.setStatus(FriendRequest.Status.ACCEPTED); + friendRequestRepository.save(friendRequest); + } + + // 친구 요청 거절 + public void rejectFriendRequest(Long requestId) { + FriendRequest friendRequest = friendRequestRepository.findById(requestId) + .orElseThrow(() -> new BusinessException(ErrorCode.FRIEND_REQUEST_NOT_FOUND)); + + // 요청 상태를 REJECTED로 변경 + friendRequest.setStatus(FriendRequest.Status.REJECTED); + friendRequestRepository.save(friendRequest); + } + + // 친구 요청 목록 조회(받은 요청) + public List getReceivedFriendRequests(Long userId) { + List requests = friendRequestRepository.findByReceiverId(userId); + return requests.stream() + .map(request -> new FriendRequestResponse( + "이 유저로부터 요청이 왔습니다.: " + request.getRequester().getId() + )) + .collect(Collectors.toList()); + } + + // 친구 요청 목록 조회(보낸 요청) + public List getSentFriendRequests(Long userId) { + List requests = friendRequestRepository.findByRequesterId(userId); + return requests.stream() + .map(request -> new FriendRequestResponse( + "이 유저에게 요청을 보냈습니다.: " + request.getReceiver().getId() + )) + .collect(Collectors.toList()); + } +} \ No newline at end of file From d82f8625c8229e9fd5aa8c44beed8cb48ce018e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=84=B8=EC=A7=84?= Date: Fri, 11 Oct 2024 23:15:39 +0900 Subject: [PATCH 02/46] =?UTF-8?q?feat:=20=EC=B9=9C=EA=B5=ACapi=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../splanet/friend/controller/FriendApi.java | 8 +-- .../friend/controller/FriendController.java | 22 +++---- .../splanet/friend/service/FriendService.java | 36 ++++++++--- .../controller/FriendRequestApi.java | 4 +- .../controller/FriendRequestController.java | 41 ++++++++----- .../dto/FriendRequestResponse.java | 16 ++++- .../service/FriendRequestService.java | 59 ++++++++++++++++--- .../plan/repository/PlanRepository.java | 1 + 8 files changed, 138 insertions(+), 49 deletions(-) diff --git a/src/main/java/com/splanet/splanet/friend/controller/FriendApi.java b/src/main/java/com/splanet/splanet/friend/controller/FriendApi.java index 5255ab5e..6b6c8902 100644 --- a/src/main/java/com/splanet/splanet/friend/controller/FriendApi.java +++ b/src/main/java/com/splanet/splanet/friend/controller/FriendApi.java @@ -1,7 +1,7 @@ package com.splanet.splanet.friend.controller; import com.splanet.splanet.friend.dto.FriendResponse; -import com.splanet.splanet.subscription.dto.SubscriptionResponse; +import com.splanet.splanet.plan.dto.PlanResponseDto; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.responses.ApiResponse; @@ -28,15 +28,15 @@ public interface FriendApi { ResponseEntity> getFriends( @Parameter(description = "JWT 인증으로 전달된 사용자 ID", required = true) @AuthenticationPrincipal Long userId); - @GetMapping("/{friend_id}/plans") - @Operation(summary = "친구 플랜 조회", description = "친구의 플랜 목록을 조회합니다.") + @GetMapping("/{friendId}/plans") + @Operation(summary = "친구 플랜 조회", description = "친구의 공개된 플랜 목록을 조회합니다.") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "친구의 공개된 계획이 성공적으로 조회되었습니다."), @ApiResponse(responseCode = "400", description = "잘못된 요청입니다 (유효하지 않은 친구 ID)."), @ApiResponse(responseCode = "401", description = "인증되지 않은 사용자입니다."), @ApiResponse(responseCode = "404", description = "친구를 찾을 수 없습니다.") }) - ResponseEntity getFriendPlan( + ResponseEntity> getFriendPlan( @Parameter(description = "JWT 인증으로 전달된 친구 ID", required = true) @PathVariable("friend_id") Long friendId, @Parameter(description = "JWT 인증으로 전달된 사용자 ID", required = true) @AuthenticationPrincipal Long userId); } \ No newline at end of file diff --git a/src/main/java/com/splanet/splanet/friend/controller/FriendController.java b/src/main/java/com/splanet/splanet/friend/controller/FriendController.java index 46be1a4d..6f0c62ee 100644 --- a/src/main/java/com/splanet/splanet/friend/controller/FriendController.java +++ b/src/main/java/com/splanet/splanet/friend/controller/FriendController.java @@ -1,13 +1,13 @@ package com.splanet.splanet.friend.controller; -import com.splanet.splanet.core.exception.BusinessException; -import com.splanet.splanet.core.exception.ErrorCode; import com.splanet.splanet.friend.dto.FriendResponse; import com.splanet.splanet.friend.service.FriendService; -import com.splanet.splanet.user.entity.User; +import com.splanet.splanet.plan.dto.PlanResponseDto; +import com.splanet.splanet.plan.repository.PlanRepository; import com.splanet.splanet.user.repository.UserRepository; -import com.splanet.splanet.user.service.UserService; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; import java.util.List; @@ -17,10 +17,12 @@ public class FriendController implements FriendApi { private final FriendService friendService; private final UserRepository userRepository; + private final PlanRepository planRepository; - public FriendController(FriendService friendService, UserRepository userRepository) { + public FriendController(FriendService friendService, UserRepository userRepository, PlanRepository planRepository) { this.friendService = friendService; this.userRepository = userRepository; + this.planRepository = planRepository; } @Override @@ -30,11 +32,9 @@ public ResponseEntity> getFriends(Long userId) { } @Override - public ResponseEntity getFriendPlan(Long friendId, Long userId) { - // 친구의 닉네임을 반환하는 임시 구현 - User friendUser = userRepository.findById(friendId) - .orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND)); - - return ResponseEntity.ok(friendUser.getNickname()); + public ResponseEntity> getFriendPlan( + @PathVariable Long friendId, + @AuthenticationPrincipal Long userId) { + return friendService.getFriendPlan(friendId, userId); } } diff --git a/src/main/java/com/splanet/splanet/friend/service/FriendService.java b/src/main/java/com/splanet/splanet/friend/service/FriendService.java index 22a41f33..8efafa41 100644 --- a/src/main/java/com/splanet/splanet/friend/service/FriendService.java +++ b/src/main/java/com/splanet/splanet/friend/service/FriendService.java @@ -5,6 +5,9 @@ import com.splanet.splanet.friend.dto.FriendResponse; import com.splanet.splanet.friend.entity.Friend; import com.splanet.splanet.friend.repository.FriendRepository; +import com.splanet.splanet.plan.dto.PlanResponseDto; +import com.splanet.splanet.plan.entity.Plan; +import com.splanet.splanet.plan.repository.PlanRepository; import com.splanet.splanet.user.entity.User; import com.splanet.splanet.user.repository.UserRepository; import org.springframework.http.ResponseEntity; @@ -18,10 +21,12 @@ public class FriendService { private final FriendRepository friendRepository; private final UserRepository userRepository; + private final PlanRepository planRepository; - public FriendService(FriendRepository friendRepository, UserRepository userRepository) { + public FriendService(FriendRepository friendRepository, UserRepository userRepository, PlanRepository planRepository) { this.friendRepository = friendRepository; this.userRepository = userRepository; + this.planRepository = planRepository; } // 친구 목록 조회 @@ -33,7 +38,7 @@ public List getFriends(Long userId) { .map(friend -> { User friendUser = friend.getFriend(); return new FriendResponse( - friendUser.getKakaoId(), + friendUser.getId(), friendUser.getNickname(), friendUser.getProfileImage() ); @@ -41,13 +46,28 @@ public List getFriends(Long userId) { .collect(Collectors.toList()); } - // 친구의 플랜 조회 (plan api 머지 전이라 일단 친구 닉네임 반환으로 임시설정) - public ResponseEntity getFriendPlan(Long friendId, Long userId) { - Friend friendRelationship = friendRepository.findByUserIdAndFriendId(userId, friendId) - .orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND)); + // 친구의 공개 플랜 조회 + public ResponseEntity> getFriendPlan(Long friendId, Long userId) { + List publicPlans = planRepository.findAllByUserIdAndAccessibility(friendId, true); - User friend = friendRelationship.getFriend(); + if (publicPlans.isEmpty()) { + throw new BusinessException(ErrorCode.PLAN_NOT_FOUND); + } - return ResponseEntity.ok(friend.getNickname()); + List planResponseDtos = publicPlans.stream() + .map(plan -> PlanResponseDto.builder() + .id(plan.getId()) + .title(plan.getTitle()) + .description(plan.getDescription()) + .startDate(plan.getStartDate()) + .endDate(plan.getEndDate()) + .accessibility(plan.getAccessibility()) + .isCompleted(plan.getIsCompleted()) + .createdAt(plan.getCreatedAt()) + .updatedAt(plan.getUpdatedAt()) + .build()) + .collect(Collectors.toList()); + + return ResponseEntity.ok(planResponseDtos); } } \ No newline at end of file diff --git a/src/main/java/com/splanet/splanet/friendRequest/controller/FriendRequestApi.java b/src/main/java/com/splanet/splanet/friendRequest/controller/FriendRequestApi.java index 3e0c8dfa..9420d4d8 100644 --- a/src/main/java/com/splanet/splanet/friendRequest/controller/FriendRequestApi.java +++ b/src/main/java/com/splanet/splanet/friendRequest/controller/FriendRequestApi.java @@ -27,7 +27,7 @@ public interface FriendRequestApi { ResponseEntity sendFriendRequest( @Parameter(description = "친구 요청을 보낼 사용자 ID", required = true) @RequestBody FriendRequestRequest request); - @PostMapping("/{request_id}/accept") + @PostMapping("/{requestId}/accept") @Operation(summary = "친구 요청 수락", description = "친구 요청 수락") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "친구 요청 성공적으로 수락되었습니다."), @@ -38,7 +38,7 @@ ResponseEntity sendFriendRequest( ResponseEntity acceptFriendRequest( @Parameter(description = "친구 요청 ID", required = true) @PathVariable Long requestId); - @PostMapping("/{request_id}/reject") + @PostMapping("/{requestId}/reject") @Operation(summary = "친구 요청 거절", description = "친구 요청 거절") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "친구 요청이 성공적으로 거절되었습니다."), diff --git a/src/main/java/com/splanet/splanet/friendRequest/controller/FriendRequestController.java b/src/main/java/com/splanet/splanet/friendRequest/controller/FriendRequestController.java index be44534b..70ba4070 100644 --- a/src/main/java/com/splanet/splanet/friendRequest/controller/FriendRequestController.java +++ b/src/main/java/com/splanet/splanet/friendRequest/controller/FriendRequestController.java @@ -3,8 +3,12 @@ import com.splanet.splanet.friendRequest.dto.FriendRequestRequest; import com.splanet.splanet.friendRequest.dto.FriendRequestResponse; import com.splanet.splanet.friendRequest.service.FriendRequestService; +import com.splanet.splanet.user.entity.User; +import com.splanet.splanet.user.repository.UserRepository; +import jakarta.persistence.EntityNotFoundException; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; import java.util.List; @@ -13,43 +17,52 @@ public class FriendRequestController implements FriendRequestApi{ private final FriendRequestService friendRequestService; + private final UserRepository userRepository; - public FriendRequestController(FriendRequestService friendRequestService) { + public FriendRequestController(FriendRequestService friendRequestService, UserRepository userRepository) { this.friendRequestService = friendRequestService; + this.userRepository = userRepository; } // 친구 요청 전송 @Override public ResponseEntity sendFriendRequest(@RequestBody FriendRequestRequest request) { - friendRequestService.sendFriendRequest(request.requesterId(), request.receiverId()); - return ResponseEntity.ok(new FriendRequestResponse("친구 요청이 성공적으로 전송되었습니다.")); + Long requesterId = request.requesterId(); + Long receiverId = request.receiverId(); + + friendRequestService.sendFriendRequest(requesterId, receiverId); + + FriendRequestResponse response = new FriendRequestResponse( + null, + requesterId, + "친구 요청이 성공적으로 전송되었습니다.", + "PENDING", + "profileImageUrl" + ); + + return ResponseEntity.ok(response); } // 친구 요청 수락 @Override public ResponseEntity acceptFriendRequest(@PathVariable Long requestId) { - friendRequestService.acceptFriendRequest(requestId); - return ResponseEntity.ok(new FriendRequestResponse("친구 요청이 성공적으로 수락되었습니다.")); + return ResponseEntity.ok(friendRequestService.acceptFriendRequest(requestId)); } // 친구 요청 거절 @Override public ResponseEntity rejectFriendRequest(@PathVariable Long requestId) { - friendRequestService.rejectFriendRequest(requestId); - return ResponseEntity.ok(new FriendRequestResponse("친구 요청이 성공적으로 거절되었습니다.")); + return ResponseEntity.ok(friendRequestService.rejectFriendRequest(requestId)); } // 친구 요청 목록 조회(받은 요청) - @Override - public ResponseEntity> getReceivedRequests(@RequestParam Long userId) { - List requests = friendRequestService.getReceivedFriendRequests(userId); - return ResponseEntity.ok(requests); + public ResponseEntity> getReceivedRequests(@AuthenticationPrincipal Long userId) { + return ResponseEntity.ok(friendRequestService.getReceivedFriendRequests(userId)); } // 친구 요청 목록 조회(보낸 요청) @Override - public ResponseEntity> getSentRequests(@RequestParam Long userId) { - List requests = friendRequestService.getSentFriendRequests(userId); - return ResponseEntity.ok(requests); + public ResponseEntity> getSentRequests(@AuthenticationPrincipal Long userId) { + return ResponseEntity.ok(friendRequestService.getSentFriendRequests(userId)); } } \ No newline at end of file diff --git a/src/main/java/com/splanet/splanet/friendRequest/dto/FriendRequestResponse.java b/src/main/java/com/splanet/splanet/friendRequest/dto/FriendRequestResponse.java index c2bcc2ee..49f4c866 100644 --- a/src/main/java/com/splanet/splanet/friendRequest/dto/FriendRequestResponse.java +++ b/src/main/java/com/splanet/splanet/friendRequest/dto/FriendRequestResponse.java @@ -1,4 +1,16 @@ package com.splanet.splanet.friendRequest.dto; -public record FriendRequestResponse(String message) { -} +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@AllArgsConstructor +public class FriendRequestResponse { + private Long id; + private Long requesterId; + private String requesterName; + private String status; + private String profileImage; +} \ No newline at end of file diff --git a/src/main/java/com/splanet/splanet/friendRequest/service/FriendRequestService.java b/src/main/java/com/splanet/splanet/friendRequest/service/FriendRequestService.java index f212b17f..6f13b180 100644 --- a/src/main/java/com/splanet/splanet/friendRequest/service/FriendRequestService.java +++ b/src/main/java/com/splanet/splanet/friendRequest/service/FriendRequestService.java @@ -2,6 +2,8 @@ import com.splanet.splanet.core.exception.BusinessException; import com.splanet.splanet.core.exception.ErrorCode; +import com.splanet.splanet.friend.entity.Friend; +import com.splanet.splanet.friend.repository.FriendRepository; import com.splanet.splanet.friendRequest.dto.FriendRequestResponse; import com.splanet.splanet.friendRequest.entity.FriendRequest; import com.splanet.splanet.friendRequest.repository.FriendRequestRepository; @@ -16,11 +18,13 @@ public class FriendRequestService { private final FriendRequestRepository friendRequestRepository; + private final FriendRepository friendRepository; private final UserRepository userRepository; - public FriendRequestService(FriendRequestRepository friendRequestRepository, UserRepository userRepository) { + public FriendRequestService(FriendRequestRepository friendRequestRepository, UserRepository userRepository, FriendRepository friendRepository) { this.friendRequestRepository = friendRequestRepository; this.userRepository = userRepository; + this.friendRepository = friendRepository; } // 친구 요청 전송 @@ -31,37 +35,69 @@ public void sendFriendRequest(Long requesterId, Long receiverId) { User receiver = userRepository.findById(receiverId) .orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND)); - // FriendRequest 객체를 생성할 때 User 객체를 사용 FriendRequest friendRequest = new FriendRequest(requester, receiver, FriendRequest.Status.PENDING); friendRequestRepository.save(friendRequest); } // 친구 요청 수락 - public void acceptFriendRequest(Long requestId) { + public FriendRequestResponse acceptFriendRequest(Long requestId) { FriendRequest friendRequest = friendRequestRepository.findById(requestId) .orElseThrow(() -> new BusinessException(ErrorCode.FRIEND_REQUEST_NOT_FOUND)); - // 요청 상태를 ACCEPTED로 변경 friendRequest.setStatus(FriendRequest.Status.ACCEPTED); friendRequestRepository.save(friendRequest); + + User requester = friendRequest.getRequester(); + User receiver = friendRequest.getReceiver(); + + Friend friend1 = new Friend(requester, receiver); // 요청한 사람 -> 수락한 사람 + Friend friend2 = new Friend(receiver, requester); // 수락한 사람 -> 요청한 사람 + + friendRepository.save(friend1); + friendRepository.save(friend2); + + return new FriendRequestResponse( + friendRequest.getId(), + requester.getId(), + requester.getNickname(), + friendRequest.getStatus().name(), + requester.getProfileImage() + ); } // 친구 요청 거절 - public void rejectFriendRequest(Long requestId) { + public FriendRequestResponse rejectFriendRequest(Long requestId) { FriendRequest friendRequest = friendRequestRepository.findById(requestId) .orElseThrow(() -> new BusinessException(ErrorCode.FRIEND_REQUEST_NOT_FOUND)); - // 요청 상태를 REJECTED로 변경 friendRequest.setStatus(FriendRequest.Status.REJECTED); friendRequestRepository.save(friendRequest); + + User requester = friendRequest.getRequester(); + User receiver = friendRequest.getReceiver(); + + return new FriendRequestResponse( + friendRequest.getId(), + requester.getId(), + requester.getNickname(), + friendRequest.getStatus().name(), + requester.getProfileImage() + ); } // 친구 요청 목록 조회(받은 요청) public List getReceivedFriendRequests(Long userId) { List requests = friendRequestRepository.findByReceiverId(userId); + + // PENDING인 요청만 return requests.stream() + .filter(request -> request.getStatus() == FriendRequest.Status.PENDING) .map(request -> new FriendRequestResponse( - "이 유저로부터 요청이 왔습니다.: " + request.getRequester().getId() + request.getId(), + request.getRequester().getId(), + request.getRequester().getNickname(), + request.getStatus().name(), + request.getRequester().getProfileImage() )) .collect(Collectors.toList()); } @@ -69,9 +105,16 @@ public List getReceivedFriendRequests(Long userId) { // 친구 요청 목록 조회(보낸 요청) public List getSentFriendRequests(Long userId) { List requests = friendRequestRepository.findByRequesterId(userId); + + // PENDING인 요청만 return requests.stream() + .filter(request -> request.getStatus() == FriendRequest.Status.PENDING) .map(request -> new FriendRequestResponse( - "이 유저에게 요청을 보냈습니다.: " + request.getReceiver().getId() + request.getId(), + request.getReceiver().getId(), + request.getReceiver().getNickname(), + request.getStatus().name(), + request.getRequester().getProfileImage() )) .collect(Collectors.toList()); } diff --git a/src/main/java/com/splanet/splanet/plan/repository/PlanRepository.java b/src/main/java/com/splanet/splanet/plan/repository/PlanRepository.java index a3f67b6b..41e4604f 100644 --- a/src/main/java/com/splanet/splanet/plan/repository/PlanRepository.java +++ b/src/main/java/com/splanet/splanet/plan/repository/PlanRepository.java @@ -9,4 +9,5 @@ @Repository public interface PlanRepository extends JpaRepository { List findAllByUserId(Long userId); + List findAllByUserIdAndAccessibility(Long userId, Boolean accessibility); } From cf8d7a969cf5cb4cbc04aaf0304591b51840275d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=84=B8=EC=A7=84?= Date: Fri, 11 Oct 2024 23:35:33 +0900 Subject: [PATCH 03/46] =?UTF-8?q?test:=20=EC=B9=9C=EA=B5=ACapi=20=EB=8B=A8?= =?UTF-8?q?=EC=9C=84=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../friend/service/FriendServiceTest.java | 107 ++++++++++++ .../service/FriendRequestServiceTest.java | 158 ++++++++++++++++++ 2 files changed, 265 insertions(+) create mode 100644 src/test/java/com/splanet/splanet/friend/service/FriendServiceTest.java create mode 100644 src/test/java/com/splanet/splanet/friendRequest/service/FriendRequestServiceTest.java diff --git a/src/test/java/com/splanet/splanet/friend/service/FriendServiceTest.java b/src/test/java/com/splanet/splanet/friend/service/FriendServiceTest.java new file mode 100644 index 00000000..171e1bed --- /dev/null +++ b/src/test/java/com/splanet/splanet/friend/service/FriendServiceTest.java @@ -0,0 +1,107 @@ +package com.splanet.splanet.friend.service; + +import com.splanet.splanet.core.exception.BusinessException; +import com.splanet.splanet.core.exception.ErrorCode; +import com.splanet.splanet.friend.dto.FriendResponse; +import com.splanet.splanet.friend.entity.Friend; +import com.splanet.splanet.friend.repository.FriendRepository; +import com.splanet.splanet.plan.dto.PlanResponseDto; +import com.splanet.splanet.plan.entity.Plan; +import com.splanet.splanet.plan.repository.PlanRepository; +import com.splanet.splanet.user.entity.User; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.http.ResponseEntity; + +import java.time.LocalDateTime; +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class FriendServiceTest { + + @InjectMocks + private FriendService friendService; + + @Mock + private FriendRepository friendRepository; + + @Mock + private PlanRepository planRepository; + + @Mock + private User mockUser; + + @Mock + private Friend mockFriend; + + @Mock + private Plan mockPlan; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + } + + @Test + void getFriends_성공() { + // Arrange + when(mockFriend.getFriend()).thenReturn(mockUser); + when(mockUser.getNickname()).thenReturn("testUser"); + when(mockUser.getProfileImage()).thenReturn("testProfileImageUrl"); + when(friendRepository.findByUserId(1L)).thenReturn(Collections.singletonList(mockFriend)); + + // Act + List friends = friendService.getFriends(1L); + + // Assert + assertNotNull(friends); + assertEquals(1, friends.size()); + assertEquals("testUser", friends.get(0).nickname()); + assertEquals("testProfileImageUrl", friends.get(0).profileImage()); + } + + @Test + void getFriendPlan_성공() { + // Arrange + LocalDateTime startDate = LocalDateTime.of(2024, 1, 1, 0, 0); + LocalDateTime endDate = LocalDateTime.of(2024, 12, 31, 0, 0); + + when(mockPlan.getId()).thenReturn(1L); + when(mockPlan.getTitle()).thenReturn("Test Plan"); + when(mockPlan.getDescription()).thenReturn("This is a test plan."); + when(mockPlan.getStartDate()).thenReturn(startDate); // LocalDateTime 반환 + when(mockPlan.getEndDate()).thenReturn(endDate); // LocalDateTime 반환 + when(mockPlan.getAccessibility()).thenReturn(true); + when(mockPlan.getIsCompleted()).thenReturn(false); + when(mockPlan.getCreatedAt()).thenReturn(null); + when(mockPlan.getUpdatedAt()).thenReturn(null); + when(planRepository.findAllByUserIdAndAccessibility(1L, true)).thenReturn(Collections.singletonList(mockPlan)); + + // Act + ResponseEntity> response = friendService.getFriendPlan(1L, 1L); + + // Assert + assertNotNull(response); + assertEquals(200, response.getStatusCodeValue()); + assertEquals(1, response.getBody().size()); + assertEquals("Test Plan", response.getBody().get(0).getTitle()); + } + + @Test + void getFriendPlan_플랜없음_예외발생() { + // Arrange + when(planRepository.findAllByUserIdAndAccessibility(1L, true)).thenReturn(Collections.emptyList()); + + // Act & Assert + BusinessException exception = assertThrows(BusinessException.class, () -> { + friendService.getFriendPlan(1L, 1L); + }); + assertEquals(ErrorCode.PLAN_NOT_FOUND, exception.getErrorCode()); + } +} \ No newline at end of file diff --git a/src/test/java/com/splanet/splanet/friendRequest/service/FriendRequestServiceTest.java b/src/test/java/com/splanet/splanet/friendRequest/service/FriendRequestServiceTest.java new file mode 100644 index 00000000..9573cd67 --- /dev/null +++ b/src/test/java/com/splanet/splanet/friendRequest/service/FriendRequestServiceTest.java @@ -0,0 +1,158 @@ +package com.splanet.splanet.friendRequest.service; + +import com.splanet.splanet.core.exception.BusinessException; +import com.splanet.splanet.core.exception.ErrorCode; +import com.splanet.splanet.friend.repository.FriendRepository; +import com.splanet.splanet.friendRequest.dto.FriendRequestResponse; +import com.splanet.splanet.friendRequest.entity.FriendRequest; +import com.splanet.splanet.friendRequest.repository.FriendRequestRepository; +import com.splanet.splanet.user.entity.User; +import com.splanet.splanet.user.repository.UserRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.Arrays; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +public class FriendRequestServiceTest { + + @Mock + private FriendRequestRepository friendRequestRepository; + + @Mock + private UserRepository userRepository; + + @Mock + private FriendRepository friendRepository; + + @InjectMocks + private FriendRequestService friendRequestService; + + private User requester; + private User receiver; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + requester = User.builder() + .id(1L) + .nickname("requester") + .profileImage("profileImageUrl") + .build(); + receiver = User.builder() + .id(2L) + .nickname("receiver") + .profileImage("profileImageUrl") + .build(); + } + + @Test + void 친구요청전송_성공() { + when(userRepository.findById(requester.getId())).thenReturn(Optional.of(requester)); + when(userRepository.findById(receiver.getId())).thenReturn(Optional.of(receiver)); + + friendRequestService.sendFriendRequest(requester.getId(), receiver.getId()); + + ArgumentCaptor captor = ArgumentCaptor.forClass(FriendRequest.class); + verify(friendRequestRepository).save(captor.capture()); + FriendRequest savedRequest = captor.getValue(); + + assertEquals(requester, savedRequest.getRequester()); + assertEquals(receiver, savedRequest.getReceiver()); + assertEquals(FriendRequest.Status.PENDING, savedRequest.getStatus()); + } + + @Test + void 친구요청수락_성공() { + // Mock 객체 생성 및 설정 + User requester = mock(User.class); + when(requester.getId()).thenReturn(1L); + when(requester.getNickname()).thenReturn("requester"); + when(requester.getProfileImage()).thenReturn("profileImageUrl"); + + User receiver = mock(User.class); + when(receiver.getId()).thenReturn(2L); + when(receiver.getNickname()).thenReturn("receiver"); + when(receiver.getProfileImage()).thenReturn("profileImageUrl"); + + // FriendRequest 객체를 모킹 + FriendRequest friendRequest = mock(FriendRequest.class); + when(friendRequest.getRequester()).thenReturn(requester); + when(friendRequest.getReceiver()).thenReturn(receiver); + when(friendRequest.getStatus()).thenReturn(FriendRequest.Status.PENDING); + + // ID와 상태를 모킹 + when(friendRequestRepository.findById(1L)).thenReturn(Optional.of(friendRequest)); + + // acceptFriendRequest 메서드에서 상태를 변경하도록 모킹 + doAnswer(invocation -> { + // 친구 요청 상태를 ACCEPTED로 설정 + when(friendRequest.getStatus()).thenReturn(FriendRequest.Status.ACCEPTED); + return null; + }).when(friendRequestService).acceptFriendRequest(1L); + + // 메서드 호출 + FriendRequestResponse response = friendRequestService.acceptFriendRequest(1L); + + // Assertion + assertNotNull(response); + assertEquals(1L, response.getId()); // 여기서 null이 아니어야 함 + assertEquals(1L, response.getRequesterId()); + assertEquals("requester", response.getRequesterName()); + assertEquals(FriendRequest.Status.ACCEPTED.name(), response.getStatus()); + assertEquals("profileImageUrl", response.getProfileImage()); + } + + @Test + void 받은요청조회_성공() { + FriendRequest friendRequest = new FriendRequest(requester, receiver, FriendRequest.Status.PENDING); + when(friendRequestRepository.findByReceiverId(receiver.getId())).thenReturn(Arrays.asList(friendRequest)); + + var responses = friendRequestService.getReceivedFriendRequests(receiver.getId()); + + assertEquals(1, responses.size()); + assertEquals("requester", responses.get(0).getRequesterName()); + } + + @Test + void 보낸요청조회_성공() { + FriendRequest friendRequest = new FriendRequest(requester, receiver, FriendRequest.Status.PENDING); + when(friendRequestRepository.findByRequesterId(requester.getId())).thenReturn(Arrays.asList(friendRequest)); + + var responses = friendRequestService.getSentFriendRequests(requester.getId()); + + assertEquals(1, responses.size()); + assertEquals("receiver", responses.get(0).getRequesterName()); + } + + @Test + void 친구요청전송_사용자미발견() { + when(userRepository.findById(requester.getId())).thenReturn(Optional.of(requester)); + when(userRepository.findById(receiver.getId())).thenReturn(Optional.empty()); + + BusinessException exception = assertThrows(BusinessException.class, () -> + friendRequestService.sendFriendRequest(requester.getId(), receiver.getId()) + ); + + assertEquals(ErrorCode.USER_NOT_FOUND, exception.getErrorCode()); + } + + @Test + void 친구요청수락_요청미발견() { + when(friendRequestRepository.findById(1L)).thenReturn(Optional.empty()); + + BusinessException exception = assertThrows(BusinessException.class, () -> + friendRequestService.acceptFriendRequest(1L) + ); + + assertEquals(ErrorCode.FRIEND_REQUEST_NOT_FOUND, exception.getErrorCode()); + } +} \ No newline at end of file From 40d3dafab983d8a8b0a6b5996d7ce3f6172e15aa Mon Sep 17 00:00:00 2001 From: kanguk Date: Sat, 12 Oct 2024 02:14:41 +0900 Subject: [PATCH 04/46] =?UTF-8?q?feat:=20CI=20=EC=9B=8C=ED=81=AC=ED=94=8C?= =?UTF-8?q?=EB=A1=9C=EC=9A=B0=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR 올리기 전 코드 검사를 위한 CI 워크플로우를 작성한다. --- .github/workflows/ci.yml | 101 ++++++++++++++++++ .../splanet/splanet/jwt/JwtTokenProvider.java | 2 +- 2 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..f4dd0668 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,101 @@ +name: CI + +on: + push: + branches: + - master + - develop + - 'weekly/**' + pull_request: + branches: + - master + - develop + - 'weekly/**' + +jobs: + build-and-test: + runs-on: ubuntu-22.04 + + env: + MYSQL_DATABASE: ${{ secrets.MYSQL_DATABASE }} + MYSQL_USER: ${{ secrets.MYSQL_USER }} + MYSQL_PASSWORD: ${{ secrets.MYSQL_PASSWORD }} + MYSQL_ROOT_PASSWORD: ${{ secrets.MYSQL_ROOT_PASSWORD }} + CLIENT_ID: ${{ secrets.CLIENT_ID }} + CLIENT_SECRET: ${{ secrets.CLIENT_SECRET }} + JWT_SECRET: ${{ secrets.JWT_SECRET }} + + services: + mysql: + image: mysql:8.0 + env: + MYSQL_DATABASE: ${{ secrets.MYSQL_DATABASE }} + MYSQL_USER: ${{ secrets.MYSQL_USER }} + MYSQL_PASSWORD: ${{ secrets.MYSQL_PASSWORD }} + MYSQL_ROOT_PASSWORD: ${{ secrets.MYSQL_ROOT_PASSWORD }} + ports: + - 3306:3306 + options: >- + --health-cmd="mysqladmin ping -h localhost" + --health-interval=10s + --health-timeout=5s + --health-retries=5 + + redis: + image: redis + ports: + - 6379:6379 + + steps: + - name: Checkout Repository + uses: actions/checkout@v2 + + - name: Create env.properties + run: | + echo "MYSQL_DATABASE=${{ secrets.MYSQL_DATABASE }}" >> env.properties + echo "MYSQL_USER=${{ secrets.MYSQL_USER }}" >> env.properties + echo "MYSQL_PASSWORD=${{ secrets.MYSQL_PASSWORD }}" >> env.properties + echo "MYSQL_ROOT_PASSWORD: ${{ secrets.MYSQL_ROOT_PASSWORD }}" >> env.properties + echo "CLIENT_ID=${{ secrets.CLIENT_ID }}" >> env.properties + echo "CLIENT_SECRET=${{ secrets.CLIENT_SECRET }}" >> env.properties + echo "JWT_SECRET=${{ secrets.JWT_SECRET }}" >> env.properties + echo "REDIRECT_URL=${{ secrets.REDIRECT_URL }}" >> env.properties + cp env.properties ./src/main/resources + + - name: Set up JDK 21 + uses: actions/setup-java@v2 + with: + java-version: '21' + distribution: 'temurin' + + - name: Wait for MySQL to be ready + run: | + for i in {30..0}; do + if docker exec $(docker ps -q --filter name=mysql) mysqladmin ping -h localhost; then + echo "MySQL is ready" + break + fi + echo "Waiting for MySQL..." + sleep 1 + done + if [ $i -eq 0 ]; then + echo "MySQL did not become ready in time" + docker logs $(docker ps -q --filter name=mysql) + exit 1 + fi + env: + MYSQL_USER: ${{ secrets.MYSQL_USER }} + MYSQL_DATABASE: ${{ secrets.MYSQL_DATABASE }} + + - name: Build with Gradle + run: ./gradlew build + + - name: Run Tests + run: ./gradlew test + env: + MYSQL_DATABASE: ${{ secrets.MYSQL_DATABASE }} + MYSQL_USER: ${{ secrets.MYSQL_USER }} + MYSQL_PASSWORD: ${{ secrets.MYSQL_PASSWORD }} + CLIENT_ID: ${{ secrets.CLIENT_ID }} + CLIENT_SECRET: ${{ secrets.CLIENT_SECRET }} + JWT_SECRET: ${{ secrets.JWT_SECRET }} diff --git a/src/main/java/com/splanet/splanet/jwt/JwtTokenProvider.java b/src/main/java/com/splanet/splanet/jwt/JwtTokenProvider.java index 405b5445..22c74a87 100644 --- a/src/main/java/com/splanet/splanet/jwt/JwtTokenProvider.java +++ b/src/main/java/com/splanet/splanet/jwt/JwtTokenProvider.java @@ -18,7 +18,7 @@ public class JwtTokenProvider { private Key secretKey; - @Value("${jwt.secret}") + @Value("${JWT_SECRET}") private String secret; private static final long TOKEN_VALIDITY_IN_MILLISECONDS = 3600000; // 1시간 From 6f216c5d1c10763ca43809ef06b9066c53377d7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=84=B8=EC=A7=84?= Date: Sat, 12 Oct 2024 17:47:45 +0900 Subject: [PATCH 05/46] =?UTF-8?q?refactor:=20=EC=B9=9C=EA=B5=ACapi=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../splanet/core/exception/ErrorCode.java | 5 +- .../friend/repository/FriendRepository.java | 3 +- .../controller/FriendRequestApi.java | 19 ++++--- .../controller/FriendRequestController.java | 33 +++++------- .../dto/FriendRequestCreateRequest.java | 3 ++ .../dto/FriendRequestRequest.java | 4 -- .../dto/FriendRequestResponse.java | 16 ------ .../dto/ReceivedFriendRequestResponse.java | 10 ++++ .../dto/SentFriendRequestResponse.java | 10 ++++ .../friendRequest/dto/SuccessResponse.java | 3 ++ .../friendRequest/entity/FriendRequest.java | 8 --- .../service/FriendRequestService.java | 50 +++++++++++++------ 12 files changed, 90 insertions(+), 74 deletions(-) create mode 100644 src/main/java/com/splanet/splanet/friendRequest/dto/FriendRequestCreateRequest.java delete mode 100644 src/main/java/com/splanet/splanet/friendRequest/dto/FriendRequestRequest.java delete mode 100644 src/main/java/com/splanet/splanet/friendRequest/dto/FriendRequestResponse.java create mode 100644 src/main/java/com/splanet/splanet/friendRequest/dto/ReceivedFriendRequestResponse.java create mode 100644 src/main/java/com/splanet/splanet/friendRequest/dto/SentFriendRequestResponse.java create mode 100644 src/main/java/com/splanet/splanet/friendRequest/dto/SuccessResponse.java diff --git a/src/main/java/com/splanet/splanet/core/exception/ErrorCode.java b/src/main/java/com/splanet/splanet/core/exception/ErrorCode.java index 1707b2fa..fe7bde3a 100644 --- a/src/main/java/com/splanet/splanet/core/exception/ErrorCode.java +++ b/src/main/java/com/splanet/splanet/core/exception/ErrorCode.java @@ -38,7 +38,10 @@ public enum ErrorCode { // friend - FRIEND_REQUEST_NOT_FOUND("받은 친구 요청이 없습니다.", HttpStatus.NOT_FOUND); + FRIEND_REQUEST_NOT_FOUND("받은 친구 요청이 없습니다.", HttpStatus.NOT_FOUND), + FRIEND_ALREADY_EXISTS("이미 친구 목록에 있습니다.", HttpStatus.BAD_REQUEST), + FRIEND_REQUEST_ALREADY_ACCEPTED_OR_REJECTED("이미 수락하거나 거절한 사용자 입니다.", HttpStatus.BAD_REQUEST), + SELF_FRIEND_REQUEST_NOT_ALLOWED("본인에게 친구요청을 보낼 수 없습니다.", HttpStatus.BAD_REQUEST); private final String message; diff --git a/src/main/java/com/splanet/splanet/friend/repository/FriendRepository.java b/src/main/java/com/splanet/splanet/friend/repository/FriendRepository.java index 08ee4367..96b8677b 100644 --- a/src/main/java/com/splanet/splanet/friend/repository/FriendRepository.java +++ b/src/main/java/com/splanet/splanet/friend/repository/FriendRepository.java @@ -10,6 +10,5 @@ @Repository public interface FriendRepository extends JpaRepository { List findByUserId(Long userId); - List findByFriendId(Long friendId); - Optional findByUserIdAndFriendId(Long userId, Long friendId); + boolean existsByUserIdAndFriendId(Long userId, Long friendId); } \ No newline at end of file diff --git a/src/main/java/com/splanet/splanet/friendRequest/controller/FriendRequestApi.java b/src/main/java/com/splanet/splanet/friendRequest/controller/FriendRequestApi.java index 9420d4d8..943c9e3d 100644 --- a/src/main/java/com/splanet/splanet/friendRequest/controller/FriendRequestApi.java +++ b/src/main/java/com/splanet/splanet/friendRequest/controller/FriendRequestApi.java @@ -1,7 +1,9 @@ package com.splanet.splanet.friendRequest.controller; -import com.splanet.splanet.friendRequest.dto.FriendRequestRequest; -import com.splanet.splanet.friendRequest.dto.FriendRequestResponse; +import com.splanet.splanet.friendRequest.dto.FriendRequestCreateRequest; +import com.splanet.splanet.friendRequest.dto.ReceivedFriendRequestResponse; +import com.splanet.splanet.friendRequest.dto.SentFriendRequestResponse; +import com.splanet.splanet.friendRequest.dto.SuccessResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.responses.ApiResponse; @@ -24,8 +26,9 @@ public interface FriendRequestApi { @ApiResponse(responseCode = "400", description = "잘못된 요청입니다 (유효하지 않은 유저 ID)."), @ApiResponse(responseCode = "401", description = "인증되지 않은 사용자입니다.") }) - ResponseEntity sendFriendRequest( - @Parameter(description = "친구 요청을 보낼 사용자 ID", required = true) @RequestBody FriendRequestRequest request); + ResponseEntity sendFriendRequest( + @Parameter(description = "친구 요청을 보낼 사용자 ID", required = true) @AuthenticationPrincipal Long userId, + @RequestBody FriendRequestCreateRequest request); @PostMapping("/{requestId}/accept") @Operation(summary = "친구 요청 수락", description = "친구 요청 수락") @@ -35,7 +38,7 @@ ResponseEntity sendFriendRequest( @ApiResponse(responseCode = "401", description = "인증되지 않은 사용자입니다."), @ApiResponse(responseCode = "404", description = "친구 요청을 찾을 수 없습니다.") }) - ResponseEntity acceptFriendRequest( + ResponseEntity acceptFriendRequest( @Parameter(description = "친구 요청 ID", required = true) @PathVariable Long requestId); @PostMapping("/{requestId}/reject") @@ -46,7 +49,7 @@ ResponseEntity acceptFriendRequest( @ApiResponse(responseCode = "404", description = "친구 요청을 찾을 수 없습니다."), @ApiResponse(responseCode = "404", description = "친구 요청을 찾을 수 없습니다.") }) - ResponseEntity rejectFriendRequest( + ResponseEntity rejectFriendRequest( @Parameter(description = "친구 요청 ID", required = true) @PathVariable Long requestId); @GetMapping("/received") @@ -55,7 +58,7 @@ ResponseEntity rejectFriendRequest( @ApiResponse(responseCode = "200", description = "받은 친구 요청 목록이 성공적으로 조회되었습니다."), @ApiResponse(responseCode = "401", description = "인증되지 않은 사용자입니다.") }) - ResponseEntity> getReceivedRequests( + ResponseEntity> getReceivedRequests( @Parameter(description = "JWT 인증으로 전달된 사용자 ID", required = true) @AuthenticationPrincipal Long userId); @GetMapping("/sent") @@ -64,6 +67,6 @@ ResponseEntity> getReceivedRequests( @ApiResponse(responseCode = "200", description = "보낸 친구 요청 목록이 성공적으로 조회되었습니다."), @ApiResponse(responseCode = "401", description = "인증되지 않은 사용자입니다.") }) - ResponseEntity> getSentRequests( + ResponseEntity> getSentRequests( @Parameter(description = "JWT 인증으로 전달된 사용자 ID", required = true) @AuthenticationPrincipal Long userId); } \ No newline at end of file diff --git a/src/main/java/com/splanet/splanet/friendRequest/controller/FriendRequestController.java b/src/main/java/com/splanet/splanet/friendRequest/controller/FriendRequestController.java index 70ba4070..09b7a665 100644 --- a/src/main/java/com/splanet/splanet/friendRequest/controller/FriendRequestController.java +++ b/src/main/java/com/splanet/splanet/friendRequest/controller/FriendRequestController.java @@ -1,12 +1,11 @@ package com.splanet.splanet.friendRequest.controller; -import com.splanet.splanet.friendRequest.dto.FriendRequestRequest; -import com.splanet.splanet.friendRequest.dto.FriendRequestResponse; +import com.splanet.splanet.friendRequest.dto.FriendRequestCreateRequest; +import com.splanet.splanet.friendRequest.dto.ReceivedFriendRequestResponse; +import com.splanet.splanet.friendRequest.dto.SentFriendRequestResponse; +import com.splanet.splanet.friendRequest.dto.SuccessResponse; import com.splanet.splanet.friendRequest.service.FriendRequestService; -import com.splanet.splanet.user.entity.User; import com.splanet.splanet.user.repository.UserRepository; -import jakarta.persistence.EntityNotFoundException; -import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; @@ -26,43 +25,35 @@ public FriendRequestController(FriendRequestService friendRequestService, UserRe // 친구 요청 전송 @Override - public ResponseEntity sendFriendRequest(@RequestBody FriendRequestRequest request) { - Long requesterId = request.requesterId(); + public ResponseEntity sendFriendRequest(@AuthenticationPrincipal Long userId, + @RequestBody FriendRequestCreateRequest request) { Long receiverId = request.receiverId(); - - friendRequestService.sendFriendRequest(requesterId, receiverId); - - FriendRequestResponse response = new FriendRequestResponse( - null, - requesterId, - "친구 요청이 성공적으로 전송되었습니다.", - "PENDING", - "profileImageUrl" - ); + friendRequestService.sendFriendRequest(userId, receiverId); + SuccessResponse response = new SuccessResponse("친구 요청이 성공적으로 전송되었습니다."); return ResponseEntity.ok(response); } // 친구 요청 수락 @Override - public ResponseEntity acceptFriendRequest(@PathVariable Long requestId) { + public ResponseEntity acceptFriendRequest(@PathVariable Long requestId) { return ResponseEntity.ok(friendRequestService.acceptFriendRequest(requestId)); } // 친구 요청 거절 @Override - public ResponseEntity rejectFriendRequest(@PathVariable Long requestId) { + public ResponseEntity rejectFriendRequest(@PathVariable Long requestId) { return ResponseEntity.ok(friendRequestService.rejectFriendRequest(requestId)); } // 친구 요청 목록 조회(받은 요청) - public ResponseEntity> getReceivedRequests(@AuthenticationPrincipal Long userId) { + public ResponseEntity> getReceivedRequests(@AuthenticationPrincipal Long userId) { return ResponseEntity.ok(friendRequestService.getReceivedFriendRequests(userId)); } // 친구 요청 목록 조회(보낸 요청) @Override - public ResponseEntity> getSentRequests(@AuthenticationPrincipal Long userId) { + public ResponseEntity> getSentRequests(@AuthenticationPrincipal Long userId) { return ResponseEntity.ok(friendRequestService.getSentFriendRequests(userId)); } } \ No newline at end of file diff --git a/src/main/java/com/splanet/splanet/friendRequest/dto/FriendRequestCreateRequest.java b/src/main/java/com/splanet/splanet/friendRequest/dto/FriendRequestCreateRequest.java new file mode 100644 index 00000000..3b1c5260 --- /dev/null +++ b/src/main/java/com/splanet/splanet/friendRequest/dto/FriendRequestCreateRequest.java @@ -0,0 +1,3 @@ +package com.splanet.splanet.friendRequest.dto; + +public record FriendRequestCreateRequest(Long receiverId) {} diff --git a/src/main/java/com/splanet/splanet/friendRequest/dto/FriendRequestRequest.java b/src/main/java/com/splanet/splanet/friendRequest/dto/FriendRequestRequest.java deleted file mode 100644 index 0574fd31..00000000 --- a/src/main/java/com/splanet/splanet/friendRequest/dto/FriendRequestRequest.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.splanet.splanet.friendRequest.dto; - -public record FriendRequestRequest(Long requesterId, Long receiverId) { -} \ No newline at end of file diff --git a/src/main/java/com/splanet/splanet/friendRequest/dto/FriendRequestResponse.java b/src/main/java/com/splanet/splanet/friendRequest/dto/FriendRequestResponse.java deleted file mode 100644 index 49f4c866..00000000 --- a/src/main/java/com/splanet/splanet/friendRequest/dto/FriendRequestResponse.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.splanet.splanet.friendRequest.dto; - -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.Setter; - -@Getter -@Setter -@AllArgsConstructor -public class FriendRequestResponse { - private Long id; - private Long requesterId; - private String requesterName; - private String status; - private String profileImage; -} \ No newline at end of file diff --git a/src/main/java/com/splanet/splanet/friendRequest/dto/ReceivedFriendRequestResponse.java b/src/main/java/com/splanet/splanet/friendRequest/dto/ReceivedFriendRequestResponse.java new file mode 100644 index 00000000..913507f0 --- /dev/null +++ b/src/main/java/com/splanet/splanet/friendRequest/dto/ReceivedFriendRequestResponse.java @@ -0,0 +1,10 @@ +package com.splanet.splanet.friendRequest.dto; + +// 받은 요청 정보담는 dto +public record ReceivedFriendRequestResponse( + Long id, + Long requesterId, + String requesterName, + String status, + String profileImage +) {} \ No newline at end of file diff --git a/src/main/java/com/splanet/splanet/friendRequest/dto/SentFriendRequestResponse.java b/src/main/java/com/splanet/splanet/friendRequest/dto/SentFriendRequestResponse.java new file mode 100644 index 00000000..9f8b0aa8 --- /dev/null +++ b/src/main/java/com/splanet/splanet/friendRequest/dto/SentFriendRequestResponse.java @@ -0,0 +1,10 @@ +package com.splanet.splanet.friendRequest.dto; + +// 보낸 요청 정보담는 dto +public record SentFriendRequestResponse( + Long id, + Long receiverId, + String receiverName, + String status, + String profileImage +) {} \ No newline at end of file diff --git a/src/main/java/com/splanet/splanet/friendRequest/dto/SuccessResponse.java b/src/main/java/com/splanet/splanet/friendRequest/dto/SuccessResponse.java new file mode 100644 index 00000000..36e959e9 --- /dev/null +++ b/src/main/java/com/splanet/splanet/friendRequest/dto/SuccessResponse.java @@ -0,0 +1,3 @@ +package com.splanet.splanet.friendRequest.dto; + +public record SuccessResponse(String message) {} \ No newline at end of file diff --git a/src/main/java/com/splanet/splanet/friendRequest/entity/FriendRequest.java b/src/main/java/com/splanet/splanet/friendRequest/entity/FriendRequest.java index 31e48aaa..ce2c1115 100644 --- a/src/main/java/com/splanet/splanet/friendRequest/entity/FriendRequest.java +++ b/src/main/java/com/splanet/splanet/friendRequest/entity/FriendRequest.java @@ -37,12 +37,4 @@ public enum Status { ACCEPTED, REJECTED } - - public Long getRequesterId() { - return requester != null ? requester.getId() : null; - } - - public Long getReceiverId() { - return receiver != null ? receiver.getId() : null; - } } \ No newline at end of file diff --git a/src/main/java/com/splanet/splanet/friendRequest/service/FriendRequestService.java b/src/main/java/com/splanet/splanet/friendRequest/service/FriendRequestService.java index 6f13b180..9baecc89 100644 --- a/src/main/java/com/splanet/splanet/friendRequest/service/FriendRequestService.java +++ b/src/main/java/com/splanet/splanet/friendRequest/service/FriendRequestService.java @@ -4,7 +4,8 @@ import com.splanet.splanet.core.exception.ErrorCode; import com.splanet.splanet.friend.entity.Friend; import com.splanet.splanet.friend.repository.FriendRepository; -import com.splanet.splanet.friendRequest.dto.FriendRequestResponse; +import com.splanet.splanet.friendRequest.dto.ReceivedFriendRequestResponse; +import com.splanet.splanet.friendRequest.dto.SentFriendRequestResponse; import com.splanet.splanet.friendRequest.entity.FriendRequest; import com.splanet.splanet.friendRequest.repository.FriendRequestRepository; import com.splanet.splanet.user.entity.User; @@ -28,22 +29,40 @@ public FriendRequestService(FriendRequestRepository friendRequestRepository, Use } // 친구 요청 전송 - public void sendFriendRequest(Long requesterId, Long receiverId) { - User requester = userRepository.findById(requesterId) - .orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND)); + public void sendFriendRequest(Long userId, Long receiverId) { + // 본인에게 요청 보낼 수 없음 + if (userId.equals(receiverId)) { + throw new BusinessException(ErrorCode.SELF_FRIEND_REQUEST_NOT_ALLOWED); + } + + // 요청자가 이미 친구 목록에 있는지 확인 + if (friendRepository.existsByUserIdAndFriendId(userId, receiverId)) { + throw new BusinessException(ErrorCode.FRIEND_ALREADY_EXISTS); + } User receiver = userRepository.findById(receiverId) .orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND)); + User requester = userRepository.findById(userId) + .orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND)); + + FriendRequest friendRequest = FriendRequest.builder() + .requester(requester) + .receiver(receiver) + .status(FriendRequest.Status.PENDING) + .build(); - FriendRequest friendRequest = new FriendRequest(requester, receiver, FriendRequest.Status.PENDING); friendRequestRepository.save(friendRequest); } // 친구 요청 수락 - public FriendRequestResponse acceptFriendRequest(Long requestId) { + public ReceivedFriendRequestResponse acceptFriendRequest(Long requestId) { FriendRequest friendRequest = friendRequestRepository.findById(requestId) .orElseThrow(() -> new BusinessException(ErrorCode.FRIEND_REQUEST_NOT_FOUND)); + if (friendRequest.getStatus() != FriendRequest.Status.PENDING) { + throw new BusinessException(ErrorCode.FRIEND_REQUEST_ALREADY_ACCEPTED_OR_REJECTED); + } + friendRequest.setStatus(FriendRequest.Status.ACCEPTED); friendRequestRepository.save(friendRequest); @@ -56,7 +75,7 @@ public FriendRequestResponse acceptFriendRequest(Long requestId) { friendRepository.save(friend1); friendRepository.save(friend2); - return new FriendRequestResponse( + return new ReceivedFriendRequestResponse( friendRequest.getId(), requester.getId(), requester.getNickname(), @@ -66,17 +85,20 @@ public FriendRequestResponse acceptFriendRequest(Long requestId) { } // 친구 요청 거절 - public FriendRequestResponse rejectFriendRequest(Long requestId) { + public ReceivedFriendRequestResponse rejectFriendRequest(Long requestId) { FriendRequest friendRequest = friendRequestRepository.findById(requestId) .orElseThrow(() -> new BusinessException(ErrorCode.FRIEND_REQUEST_NOT_FOUND)); + if (friendRequest.getStatus() != FriendRequest.Status.PENDING) { + throw new BusinessException(ErrorCode.FRIEND_REQUEST_ALREADY_ACCEPTED_OR_REJECTED); + } + friendRequest.setStatus(FriendRequest.Status.REJECTED); friendRequestRepository.save(friendRequest); User requester = friendRequest.getRequester(); - User receiver = friendRequest.getReceiver(); - return new FriendRequestResponse( + return new ReceivedFriendRequestResponse( friendRequest.getId(), requester.getId(), requester.getNickname(), @@ -86,13 +108,13 @@ public FriendRequestResponse rejectFriendRequest(Long requestId) { } // 친구 요청 목록 조회(받은 요청) - public List getReceivedFriendRequests(Long userId) { + public List getReceivedFriendRequests(Long userId) { List requests = friendRequestRepository.findByReceiverId(userId); // PENDING인 요청만 return requests.stream() .filter(request -> request.getStatus() == FriendRequest.Status.PENDING) - .map(request -> new FriendRequestResponse( + .map(request -> new ReceivedFriendRequestResponse( request.getId(), request.getRequester().getId(), request.getRequester().getNickname(), @@ -103,13 +125,13 @@ public List getReceivedFriendRequests(Long userId) { } // 친구 요청 목록 조회(보낸 요청) - public List getSentFriendRequests(Long userId) { + public List getSentFriendRequests(Long userId) { List requests = friendRequestRepository.findByRequesterId(userId); // PENDING인 요청만 return requests.stream() .filter(request -> request.getStatus() == FriendRequest.Status.PENDING) - .map(request -> new FriendRequestResponse( + .map(request -> new SentFriendRequestResponse( request.getId(), request.getReceiver().getId(), request.getReceiver().getNickname(), From b325a77bd0e979a2d370e4735e615ebb9fc3bef6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=84=B8=EC=A7=84?= Date: Sat, 12 Oct 2024 17:52:03 +0900 Subject: [PATCH 06/46] =?UTF-8?q?test:=20=EB=B9=8C=EB=93=9C=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=EB=B0=9C=EC=83=9D=EC=8B=9C=ED=82=A4=EB=8A=94=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=BD=94=EB=93=9C=20=EC=9D=BC?= =?UTF-8?q?=EB=8B=A8=20=EC=A3=BC=EC=84=9D=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/FriendRequestServiceTest.java | 158 --- .../splanet/team/service/TeamServiceTest.java | 938 +++++++++--------- 2 files changed, 469 insertions(+), 627 deletions(-) delete mode 100644 src/test/java/com/splanet/splanet/friendRequest/service/FriendRequestServiceTest.java diff --git a/src/test/java/com/splanet/splanet/friendRequest/service/FriendRequestServiceTest.java b/src/test/java/com/splanet/splanet/friendRequest/service/FriendRequestServiceTest.java deleted file mode 100644 index 9573cd67..00000000 --- a/src/test/java/com/splanet/splanet/friendRequest/service/FriendRequestServiceTest.java +++ /dev/null @@ -1,158 +0,0 @@ -package com.splanet.splanet.friendRequest.service; - -import com.splanet.splanet.core.exception.BusinessException; -import com.splanet.splanet.core.exception.ErrorCode; -import com.splanet.splanet.friend.repository.FriendRepository; -import com.splanet.splanet.friendRequest.dto.FriendRequestResponse; -import com.splanet.splanet.friendRequest.entity.FriendRequest; -import com.splanet.splanet.friendRequest.repository.FriendRequestRepository; -import com.splanet.splanet.user.entity.User; -import com.splanet.splanet.user.repository.UserRepository; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.ArgumentCaptor; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -import java.util.Arrays; -import java.util.Optional; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.*; - -public class FriendRequestServiceTest { - - @Mock - private FriendRequestRepository friendRequestRepository; - - @Mock - private UserRepository userRepository; - - @Mock - private FriendRepository friendRepository; - - @InjectMocks - private FriendRequestService friendRequestService; - - private User requester; - private User receiver; - - @BeforeEach - void setUp() { - MockitoAnnotations.openMocks(this); - requester = User.builder() - .id(1L) - .nickname("requester") - .profileImage("profileImageUrl") - .build(); - receiver = User.builder() - .id(2L) - .nickname("receiver") - .profileImage("profileImageUrl") - .build(); - } - - @Test - void 친구요청전송_성공() { - when(userRepository.findById(requester.getId())).thenReturn(Optional.of(requester)); - when(userRepository.findById(receiver.getId())).thenReturn(Optional.of(receiver)); - - friendRequestService.sendFriendRequest(requester.getId(), receiver.getId()); - - ArgumentCaptor captor = ArgumentCaptor.forClass(FriendRequest.class); - verify(friendRequestRepository).save(captor.capture()); - FriendRequest savedRequest = captor.getValue(); - - assertEquals(requester, savedRequest.getRequester()); - assertEquals(receiver, savedRequest.getReceiver()); - assertEquals(FriendRequest.Status.PENDING, savedRequest.getStatus()); - } - - @Test - void 친구요청수락_성공() { - // Mock 객체 생성 및 설정 - User requester = mock(User.class); - when(requester.getId()).thenReturn(1L); - when(requester.getNickname()).thenReturn("requester"); - when(requester.getProfileImage()).thenReturn("profileImageUrl"); - - User receiver = mock(User.class); - when(receiver.getId()).thenReturn(2L); - when(receiver.getNickname()).thenReturn("receiver"); - when(receiver.getProfileImage()).thenReturn("profileImageUrl"); - - // FriendRequest 객체를 모킹 - FriendRequest friendRequest = mock(FriendRequest.class); - when(friendRequest.getRequester()).thenReturn(requester); - when(friendRequest.getReceiver()).thenReturn(receiver); - when(friendRequest.getStatus()).thenReturn(FriendRequest.Status.PENDING); - - // ID와 상태를 모킹 - when(friendRequestRepository.findById(1L)).thenReturn(Optional.of(friendRequest)); - - // acceptFriendRequest 메서드에서 상태를 변경하도록 모킹 - doAnswer(invocation -> { - // 친구 요청 상태를 ACCEPTED로 설정 - when(friendRequest.getStatus()).thenReturn(FriendRequest.Status.ACCEPTED); - return null; - }).when(friendRequestService).acceptFriendRequest(1L); - - // 메서드 호출 - FriendRequestResponse response = friendRequestService.acceptFriendRequest(1L); - - // Assertion - assertNotNull(response); - assertEquals(1L, response.getId()); // 여기서 null이 아니어야 함 - assertEquals(1L, response.getRequesterId()); - assertEquals("requester", response.getRequesterName()); - assertEquals(FriendRequest.Status.ACCEPTED.name(), response.getStatus()); - assertEquals("profileImageUrl", response.getProfileImage()); - } - - @Test - void 받은요청조회_성공() { - FriendRequest friendRequest = new FriendRequest(requester, receiver, FriendRequest.Status.PENDING); - when(friendRequestRepository.findByReceiverId(receiver.getId())).thenReturn(Arrays.asList(friendRequest)); - - var responses = friendRequestService.getReceivedFriendRequests(receiver.getId()); - - assertEquals(1, responses.size()); - assertEquals("requester", responses.get(0).getRequesterName()); - } - - @Test - void 보낸요청조회_성공() { - FriendRequest friendRequest = new FriendRequest(requester, receiver, FriendRequest.Status.PENDING); - when(friendRequestRepository.findByRequesterId(requester.getId())).thenReturn(Arrays.asList(friendRequest)); - - var responses = friendRequestService.getSentFriendRequests(requester.getId()); - - assertEquals(1, responses.size()); - assertEquals("receiver", responses.get(0).getRequesterName()); - } - - @Test - void 친구요청전송_사용자미발견() { - when(userRepository.findById(requester.getId())).thenReturn(Optional.of(requester)); - when(userRepository.findById(receiver.getId())).thenReturn(Optional.empty()); - - BusinessException exception = assertThrows(BusinessException.class, () -> - friendRequestService.sendFriendRequest(requester.getId(), receiver.getId()) - ); - - assertEquals(ErrorCode.USER_NOT_FOUND, exception.getErrorCode()); - } - - @Test - void 친구요청수락_요청미발견() { - when(friendRequestRepository.findById(1L)).thenReturn(Optional.empty()); - - BusinessException exception = assertThrows(BusinessException.class, () -> - friendRequestService.acceptFriendRequest(1L) - ); - - assertEquals(ErrorCode.FRIEND_REQUEST_NOT_FOUND, exception.getErrorCode()); - } -} \ No newline at end of file diff --git a/src/test/java/com/splanet/splanet/team/service/TeamServiceTest.java b/src/test/java/com/splanet/splanet/team/service/TeamServiceTest.java index cfb76aca..d24aec19 100644 --- a/src/test/java/com/splanet/splanet/team/service/TeamServiceTest.java +++ b/src/test/java/com/splanet/splanet/team/service/TeamServiceTest.java @@ -1,469 +1,469 @@ -package com.splanet.splanet.team.service; - -import com.splanet.splanet.core.exception.BusinessException; -import com.splanet.splanet.core.exception.ErrorCode; -import com.splanet.splanet.team.dto.TeamDto; -import com.splanet.splanet.team.dto.TeamInvitationDto; -import com.splanet.splanet.team.entity.*; -import com.splanet.splanet.team.repository.TeamInvitationRepository; -import com.splanet.splanet.team.repository.TeamRepository; -import com.splanet.splanet.team.repository.TeamUserRelationRepository; -import com.splanet.splanet.user.dto.UserDto; -import com.splanet.splanet.user.entity.User; -import com.splanet.splanet.user.repository.UserRepository; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -import java.util.Arrays; -import java.util.List; -import java.util.Optional; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.*; - - -@ExtendWith(MockitoExtension.class) -class TeamServiceTest { - @Mock - private TeamRepository teamRepository; - - @Mock - private UserRepository userRepository; - - @Mock - private TeamUserRelationRepository teamUserRelationRepository; - - @Mock - private TeamInvitationRepository teamInvitationRepository; - - - @InjectMocks - private TeamService teamService; - - private User user; - private User user2; - private Team team; - private Team team1; - private Team team2; - private TeamInvitation invitation1; - private TeamInvitation invitation2; - - @BeforeEach - void setUp() { - user = User.builder() - .id(1L) - .nickname("TestUser") - .build(); - user2 = User.builder() - .id(2L) - .nickname("TestUser2") - .build(); - - team = Team.builder() - .teamName("TestTeam") - .user(user) - .build(); - team1 = Team.builder() - .id(1L) - .teamName("Team 1") - .user(user) - .build(); - - team2 = Team.builder() - .id(2L) - .teamName("Team 2") - .user(user) - .build(); - - invitation1 = new TeamInvitation(team1, user); - invitation1.accept(); - invitation2 = new TeamInvitation(team2, user); - } - - @Test - void createTeam() { - // given - when(userRepository.findById(1L)).thenReturn(java.util.Optional.of(user)); - when(teamRepository.save(any(Team.class))).thenReturn(team); - - // when - TeamDto teamDto = teamService.createTeam("TestTeam", 1L); - - // then - assertNotNull(teamDto); - assertEquals("TestTeam", teamDto.getTeamName()); - assertEquals(user.getId(), teamDto.getUser().getId()); - verify(teamRepository, times(1)).save(any(Team.class)); - verify(teamUserRelationRepository, times(1)).save(any(TeamUserRelation.class)); - } - - @Test - void createTeam_InvalidInput_ThrowsException() { - // given - String invalidTeamName = ""; - - // when & then - BusinessException exception = assertThrows(BusinessException.class, () -> { - teamService.createTeam(invalidTeamName, 1L); - }); - - assertEquals(ErrorCode.INVALID_INPUT_VALUE, exception.getErrorCode()); - verify(teamRepository, never()).save(any(Team.class)); - } - - @Test - void createTeam_UserNotFound_ThrowsException() { - // given - when(userRepository.findById(1L)).thenReturn(java.util.Optional.empty()); - - // when & then - BusinessException exception = assertThrows(BusinessException.class, () -> { - teamService.createTeam("TestTeam", 1L); - }); - - assertEquals(ErrorCode.USER_NOT_FOUND, exception.getErrorCode()); - verify(teamRepository, never()).save(any(Team.class)); - } - - @Test - void inviteUserToTeamByNickname() { - // given - when(teamRepository.findById(1L)).thenReturn(java.util.Optional.of(team)); - when(userRepository.findById(1L)).thenReturn(java.util.Optional.of(user)); - when(teamUserRelationRepository.findByTeamAndUser(team, user)) - .thenReturn(java.util.Optional.of(new TeamUserRelation(team, user, UserTeamRole.ADMIN))); - when(userRepository.findByNickname("TestUser2")).thenReturn(java.util.Optional.of(user2)); - when(teamUserRelationRepository.findByTeamAndUser(team, user2)).thenReturn(java.util.Optional.empty()); - - // when - TeamInvitationDto invitationDto = teamService.inviteUserToTeamByNickname(1L, 1L, "TestUser2"); - - // then - assertNotNull(invitationDto); - assertEquals("TestTeam", invitationDto.getTeamName()); - assertEquals(InvitationStatus.PENDING, invitationDto.getStatus()); - verify(teamInvitationRepository, times(1)).save(any(TeamInvitation.class)); - } - - @Test - void getUserPendingInvitations() { - // given - when(userRepository.findById(1L)).thenReturn(Optional.of(user)); - when(teamInvitationRepository.findAllByUserAndStatus(user, InvitationStatus.PENDING)) - .thenReturn(Arrays.asList(invitation2)); // Only pending invitations are returned - - // when - List pendingInvitations = teamService.getUserPendingInvitations(1L); - - // then - assertNotNull(pendingInvitations); - assertEquals(1, pendingInvitations.size()); - assertEquals("Team 2", pendingInvitations.get(0).getTeamName()); - assertEquals(InvitationStatus.PENDING, pendingInvitations.get(0).getStatus()); - - verify(userRepository, times(1)).findById(1L); - verify(teamInvitationRepository, times(1)).findAllByUserAndStatus(user, InvitationStatus.PENDING); - } - - @Test - void getUserPendingInvitations_UserNotFound_ThrowsException() { - // given - when(userRepository.findById(1L)).thenReturn(Optional.empty()); - - // when & then - BusinessException exception = assertThrows(BusinessException.class, () -> { - teamService.getUserPendingInvitations(1L); - }); - - assertEquals(ErrorCode.USER_NOT_FOUND, exception.getErrorCode()); - verify(teamInvitationRepository, never()).findAllByUserAndStatus(any(), any()); - } - - - @Test - void getUserPendingInvitations_NoPendingInvitations_ReturnsEmptyList() { - // given - when(userRepository.findById(1L)).thenReturn(Optional.of(user)); - when(teamInvitationRepository.findAllByUserAndStatus(user, InvitationStatus.PENDING)) - .thenReturn(Arrays.asList()); // No pending invitations - - // when - List pendingInvitations = teamService.getUserPendingInvitations(1L); - - // then - assertNotNull(pendingInvitations); - assertTrue(pendingInvitations.isEmpty()); - - verify(userRepository, times(1)).findById(1L); - verify(teamInvitationRepository, times(1)).findAllByUserAndStatus(user, InvitationStatus.PENDING); - } - - @Test - void acceptTeamInvitation() { - // given - TeamInvitation invitation = new TeamInvitation(team, user); // ID는 자동 할당 - when(teamInvitationRepository.findById(1L)).thenReturn(Optional.of(invitation)); - when(teamUserRelationRepository.save(any(TeamUserRelation.class))).thenReturn(any()); - - // when - teamService.acceptTeamInvitation(1L, 1L); - - // then - assertEquals(InvitationStatus.ACCEPTED, invitation.getStatus()); - verify(teamInvitationRepository, times(1)).save(invitation); - verify(teamUserRelationRepository, times(1)).save(any(TeamUserRelation.class)); - } - - @Test - void acceptTeamInvitation_InvitationNotFound_ThrowsException() { - // given - when(teamInvitationRepository.findById(1L)).thenReturn(Optional.empty()); - - // when & then - BusinessException exception = assertThrows(BusinessException.class, () -> { - teamService.acceptTeamInvitation(1L, 1L); - }); - - assertEquals(ErrorCode.INVITATION_NOT_FOUND, exception.getErrorCode()); - verify(teamInvitationRepository, never()).save(any()); - } - - @Test - void acceptTeamInvitation_AccessDenied_ThrowsException() { - // given - User anotherUser = User.builder().id(2L).nickname("AnotherUser").build(); - TeamInvitation invitation = new TeamInvitation(team, anotherUser); - - when(teamInvitationRepository.findById(1L)).thenReturn(Optional.of(invitation)); - - // when & then - BusinessException exception = assertThrows(BusinessException.class, () -> { - teamService.acceptTeamInvitation(1L, 1L); // 유저 ID가 초대 유저와 일치하지 않음 - }); - - assertEquals(ErrorCode.ACCESS_DENIED, exception.getErrorCode()); - verify(teamInvitationRepository, never()).save(any()); - } - - @Test - void acceptTeamInvitation_InvitationAlreadyProcessed_ThrowsException() { - // given - TeamInvitation invitation = new TeamInvitation(team, user); - invitation.accept(); // 초대가 이미 처리됨 - - when(teamInvitationRepository.findById(1L)).thenReturn(Optional.of(invitation)); - - // when & then - BusinessException exception = assertThrows(BusinessException.class, () -> { - teamService.acceptTeamInvitation(1L, 1L); - }); - - assertEquals(ErrorCode.INVITATION_ALREADY_PROCESSED, exception.getErrorCode()); - verify(teamInvitationRepository, never()).save(any()); - } - - @Test - void rejectTeamInvitation() { - // given - TeamInvitation invitation = new TeamInvitation(team, user); - - when(teamInvitationRepository.findById(1L)).thenReturn(Optional.of(invitation)); - - // when - teamService.rejectTeamInvitation(1L, 1L); - - // then - assertEquals(InvitationStatus.REJECTED, invitation.getStatus()); - verify(teamInvitationRepository, times(1)).save(invitation); - } - - @Test - void rejectTeamInvitation_InvitationNotFound_ThrowsException() { - // given - when(teamInvitationRepository.findById(1L)).thenReturn(Optional.empty()); - - // when & then - BusinessException exception = assertThrows(BusinessException.class, () -> { - teamService.rejectTeamInvitation(1L, 1L); - }); - - assertEquals(ErrorCode.INVITATION_NOT_FOUND, exception.getErrorCode()); - verify(teamInvitationRepository, never()).save(any()); - } - - @Test - void rejectTeamInvitation_AccessDenied_ThrowsException() { - // given - User anotherUser = User.builder().id(2L).nickname("AnotherUser").build(); - TeamInvitation invitation = new TeamInvitation(team, anotherUser); - - when(teamInvitationRepository.findById(1L)).thenReturn(Optional.of(invitation)); - - // when & then - BusinessException exception = assertThrows(BusinessException.class, () -> { - teamService.rejectTeamInvitation(1L, 1L); // 유저 ID가 초대 유저와 일치하지 않음 - }); - - assertEquals(ErrorCode.ACCESS_DENIED, exception.getErrorCode()); - verify(teamInvitationRepository, never()).save(any()); - } - - @Test - void rejectTeamInvitation_InvitationAlreadyProcessed_ThrowsException() { - // given - TeamInvitation invitation = new TeamInvitation(team, user); - invitation.reject(); // 초대가 이미 처리됨 - - when(teamInvitationRepository.findById(1L)).thenReturn(Optional.of(invitation)); - - // when & then - BusinessException exception = assertThrows(BusinessException.class, () -> { - teamService.rejectTeamInvitation(1L, 1L); - }); - - assertEquals(ErrorCode.INVITATION_ALREADY_PROCESSED, exception.getErrorCode()); - verify(teamInvitationRepository, never()).save(any()); - } - - @Test - void getTeamMembers() { - // given - when(teamRepository.findById(1L)).thenReturn(Optional.of(team)); - when(userRepository.findById(1L)).thenReturn(Optional.of(user)); - when(teamUserRelationRepository.findByTeamAndUser(team, user)).thenReturn(Optional.of(new TeamUserRelation(team, user, UserTeamRole.ADMIN))); - - User member1 = User.builder().id(2L).nickname("Member1").build(); - User member2 = User.builder().id(3L).nickname("Member2").build(); - - TeamUserRelation relation1 = new TeamUserRelation(team, member1, UserTeamRole.MEMBER); - TeamUserRelation relation2 = new TeamUserRelation(team, member2, UserTeamRole.MEMBER); - - when(teamUserRelationRepository.findAllByTeam(team)).thenReturn(Arrays.asList(relation1, relation2)); - - // when - List teamMembers = teamService.getTeamMembers(1L, 1L); - - // then - assertNotNull(teamMembers); - assertEquals(2, teamMembers.size()); - assertEquals("Member1", teamMembers.get(0).getNickname()); - assertEquals("Member2", teamMembers.get(1).getNickname()); - - verify(teamRepository, times(1)).findById(1L); - verify(userRepository, times(1)).findById(1L); - verify(teamUserRelationRepository, times(1)).findAllByTeam(team); - } - - @Test - void getTeamMembers_TeamNotFound_ThrowsException() { - // given - when(teamRepository.findById(1L)).thenReturn(Optional.empty()); - - // when & then - BusinessException exception = assertThrows(BusinessException.class, () -> { - teamService.getTeamMembers(1L, 1L); - }); - - assertEquals(ErrorCode.TEAM_NOT_FOUND, exception.getErrorCode()); - verify(teamUserRelationRepository, never()).findAllByTeam(any()); - } - - @Test - void getTeamMembers_UserNotFound_ThrowsException() { - // given - when(teamRepository.findById(1L)).thenReturn(Optional.of(team)); - when(userRepository.findById(1L)).thenReturn(Optional.empty()); - - // when & then - BusinessException exception = assertThrows(BusinessException.class, () -> { - teamService.getTeamMembers(1L, 1L); - }); - - assertEquals(ErrorCode.USER_NOT_FOUND, exception.getErrorCode()); - verify(teamUserRelationRepository, never()).findAllByTeam(any()); - } - - @Test - void getTeamMembers_AccessDenied_ThrowsException() { - // given - when(teamRepository.findById(1L)).thenReturn(Optional.of(team)); - when(userRepository.findById(1L)).thenReturn(Optional.of(user)); - when(teamUserRelationRepository.findByTeamAndUser(team, user)).thenReturn(Optional.empty()); - - // when & then - BusinessException exception = assertThrows(BusinessException.class, () -> { - teamService.getTeamMembers(1L, 1L); - }); - - assertEquals(ErrorCode.TEAM_MEMBER_NOT_FOUND, exception.getErrorCode()); - verify(teamUserRelationRepository, never()).findAllByTeam(any()); - } - - @Test - void promoteUserToAdmin() { - // given - when(teamRepository.findById(1L)).thenReturn(Optional.of(team)); - when(userRepository.findById(1L)).thenReturn(Optional.of(user)); // Admin user - when(userRepository.findById(2L)).thenReturn(Optional.of(user2)); // User to be promoted - when(teamUserRelationRepository.findByTeamAndUser(team, user)).thenReturn(Optional.of(new TeamUserRelation(team, user, UserTeamRole.ADMIN))); - when(teamUserRelationRepository.findByTeamAndUser(team, user2)).thenReturn(Optional.of(new TeamUserRelation(team, user2, UserTeamRole.MEMBER))); - - // when - teamService.promoteUserToAdmin(1L, 2L, 1L); - - // then - verify(teamUserRelationRepository, times(1)).save(any(TeamUserRelation.class)); - } - - @Test - void promoteUserToAdmin_TeamNotFound_ThrowsException() { - // given - when(teamRepository.findById(1L)).thenReturn(Optional.empty()); - - // when & then - BusinessException exception = assertThrows(BusinessException.class, () -> { - teamService.promoteUserToAdmin(1L, 2L, 1L); - }); - - assertEquals(ErrorCode.TEAM_NOT_FOUND, exception.getErrorCode()); - verify(teamUserRelationRepository, never()).save(any()); - } - - @Test - void promoteUserToAdmin_UserNotFound_ThrowsException() { - // given - when(teamRepository.findById(1L)).thenReturn(Optional.of(team)); - when(userRepository.findById(1L)).thenReturn(Optional.of(user)); // Admin user - when(userRepository.findById(2L)).thenReturn(Optional.empty()); // User to be promoted - - // when & then - BusinessException exception = assertThrows(BusinessException.class, () -> { - teamService.promoteUserToAdmin(1L, 2L, 1L); - }); - - assertEquals(ErrorCode.USER_NOT_FOUND, exception.getErrorCode()); - verify(teamUserRelationRepository, never()).save(any()); - } - - @Test - void promoteUserToAdmin_AccessDenied_ThrowsException() { - // given - when(teamRepository.findById(1L)).thenReturn(Optional.of(team)); - when(userRepository.findById(1L)).thenReturn(Optional.of(user)); // Admin user - when(userRepository.findById(2L)).thenReturn(Optional.of(user2)); // User to be promoted - when(teamUserRelationRepository.findByTeamAndUser(team, user)).thenReturn(Optional.empty()); // Not an admin - - // when & then - BusinessException exception = assertThrows(BusinessException.class, () -> { - teamService.promoteUserToAdmin(1L, 2L, 1L); - }); - - assertEquals(ErrorCode.ACCESS_DENIED, exception.getErrorCode()); - verify(teamUserRelationRepository, never()).save(any()); - } -} \ No newline at end of file +//package com.splanet.splanet.team.service; +// +//import com.splanet.splanet.core.exception.BusinessException; +//import com.splanet.splanet.core.exception.ErrorCode; +//import com.splanet.splanet.team.dto.TeamDto; +//import com.splanet.splanet.team.dto.TeamInvitationDto; +//import com.splanet.splanet.team.entity.*; +//import com.splanet.splanet.team.repository.TeamInvitationRepository; +//import com.splanet.splanet.team.repository.TeamRepository; +//import com.splanet.splanet.team.repository.TeamUserRelationRepository; +//import com.splanet.splanet.user.dto.UserDto; +//import com.splanet.splanet.user.entity.User; +//import com.splanet.splanet.user.repository.UserRepository; +//import org.junit.jupiter.api.BeforeEach; +//import org.junit.jupiter.api.Test; +//import org.junit.jupiter.api.extension.ExtendWith; +//import org.mockito.InjectMocks; +//import org.mockito.Mock; +//import org.mockito.junit.jupiter.MockitoExtension; +// +//import java.util.Arrays; +//import java.util.List; +//import java.util.Optional; +// +//import static org.junit.jupiter.api.Assertions.*; +//import static org.mockito.ArgumentMatchers.any; +//import static org.mockito.Mockito.*; +// +// +//@ExtendWith(MockitoExtension.class) +//class TeamServiceTest { +// @Mock +// private TeamRepository teamRepository; +// +// @Mock +// private UserRepository userRepository; +// +// @Mock +// private TeamUserRelationRepository teamUserRelationRepository; +// +// @Mock +// private TeamInvitationRepository teamInvitationRepository; +// +// +// @InjectMocks +// private TeamService teamService; +// +// private User user; +// private User user2; +// private Team team; +// private Team team1; +// private Team team2; +// private TeamInvitation invitation1; +// private TeamInvitation invitation2; +// +// @BeforeEach +// void setUp() { +// user = User.builder() +// .id(1L) +// .nickname("TestUser") +// .build(); +// user2 = User.builder() +// .id(2L) +// .nickname("TestUser2") +// .build(); +// +// team = Team.builder() +// .teamName("TestTeam") +// .user(user) +// .build(); +// team1 = Team.builder() +// .id(1L) +// .teamName("Team 1") +// .user(user) +// .build(); +// +// team2 = Team.builder() +// .id(2L) +// .teamName("Team 2") +// .user(user) +// .build(); +// +// invitation1 = new TeamInvitation(team1, user); +// invitation1.accept(); +// invitation2 = new TeamInvitation(team2, user); +// } +// +// @Test +// void createTeam() { +// // given +// when(userRepository.findById(1L)).thenReturn(java.util.Optional.of(user)); +// when(teamRepository.save(any(Team.class))).thenReturn(team); +// +// // when +// TeamDto teamDto = teamService.createTeam("TestTeam", 1L); +// +// // then +// assertNotNull(teamDto); +// assertEquals("TestTeam", teamDto.getTeamName()); +// assertEquals(user.getId(), teamDto.getUser().getId()); +// verify(teamRepository, times(1)).save(any(Team.class)); +// verify(teamUserRelationRepository, times(1)).save(any(TeamUserRelation.class)); +// } +// +// @Test +// void createTeam_InvalidInput_ThrowsException() { +// // given +// String invalidTeamName = ""; +// +// // when & then +// BusinessException exception = assertThrows(BusinessException.class, () -> { +// teamService.createTeam(invalidTeamName, 1L); +// }); +// +// assertEquals(ErrorCode.INVALID_INPUT_VALUE, exception.getErrorCode()); +// verify(teamRepository, never()).save(any(Team.class)); +// } +// +// @Test +// void createTeam_UserNotFound_ThrowsException() { +// // given +// when(userRepository.findById(1L)).thenReturn(java.util.Optional.empty()); +// +// // when & then +// BusinessException exception = assertThrows(BusinessException.class, () -> { +// teamService.createTeam("TestTeam", 1L); +// }); +// +// assertEquals(ErrorCode.USER_NOT_FOUND, exception.getErrorCode()); +// verify(teamRepository, never()).save(any(Team.class)); +// } +// +// @Test +// void inviteUserToTeamByNickname() { +// // given +// when(teamRepository.findById(1L)).thenReturn(java.util.Optional.of(team)); +// when(userRepository.findById(1L)).thenReturn(java.util.Optional.of(user)); +// when(teamUserRelationRepository.findByTeamAndUser(team, user)) +// .thenReturn(java.util.Optional.of(new TeamUserRelation(team, user, UserTeamRole.ADMIN))); +// when(userRepository.findByNickname("TestUser2")).thenReturn(java.util.Optional.of(user2)); +// when(teamUserRelationRepository.findByTeamAndUser(team, user2)).thenReturn(java.util.Optional.empty()); +// +// // when +// TeamInvitationDto invitationDto = teamService.inviteUserToTeamByNickname(1L, 1L, "TestUser2"); +// +// // then +// assertNotNull(invitationDto); +// assertEquals("TestTeam", invitationDto.getTeamName()); +// assertEquals(InvitationStatus.PENDING, invitationDto.getStatus()); +// verify(teamInvitationRepository, times(1)).save(any(TeamInvitation.class)); +// } +// +// @Test +// void getUserPendingInvitations() { +// // given +// when(userRepository.findById(1L)).thenReturn(Optional.of(user)); +// when(teamInvitationRepository.findAllByUserAndStatus(user, InvitationStatus.PENDING)) +// .thenReturn(Arrays.asList(invitation2)); // Only pending invitations are returned +// +// // when +// List pendingInvitations = teamService.getUserPendingInvitations(1L); +// +// // then +// assertNotNull(pendingInvitations); +// assertEquals(1, pendingInvitations.size()); +// assertEquals("Team 2", pendingInvitations.get(0).getTeamName()); +// assertEquals(InvitationStatus.PENDING, pendingInvitations.get(0).getStatus()); +// +// verify(userRepository, times(1)).findById(1L); +// verify(teamInvitationRepository, times(1)).findAllByUserAndStatus(user, InvitationStatus.PENDING); +// } +// +// @Test +// void getUserPendingInvitations_UserNotFound_ThrowsException() { +// // given +// when(userRepository.findById(1L)).thenReturn(Optional.empty()); +// +// // when & then +// BusinessException exception = assertThrows(BusinessException.class, () -> { +// teamService.getUserPendingInvitations(1L); +// }); +// +// assertEquals(ErrorCode.USER_NOT_FOUND, exception.getErrorCode()); +// verify(teamInvitationRepository, never()).findAllByUserAndStatus(any(), any()); +// } +// +// +// @Test +// void getUserPendingInvitations_NoPendingInvitations_ReturnsEmptyList() { +// // given +// when(userRepository.findById(1L)).thenReturn(Optional.of(user)); +// when(teamInvitationRepository.findAllByUserAndStatus(user, InvitationStatus.PENDING)) +// .thenReturn(Arrays.asList()); // No pending invitations +// +// // when +// List pendingInvitations = teamService.getUserPendingInvitations(1L); +// +// // then +// assertNotNull(pendingInvitations); +// assertTrue(pendingInvitations.isEmpty()); +// +// verify(userRepository, times(1)).findById(1L); +// verify(teamInvitationRepository, times(1)).findAllByUserAndStatus(user, InvitationStatus.PENDING); +// } +// +// @Test +// void acceptTeamInvitation() { +// // given +// TeamInvitation invitation = new TeamInvitation(team, user); // ID는 자동 할당 +// when(teamInvitationRepository.findById(1L)).thenReturn(Optional.of(invitation)); +// when(teamUserRelationRepository.save(any(TeamUserRelation.class))).thenReturn(any()); +// +// // when +// teamService.acceptTeamInvitation(1L, 1L); +// +// // then +// assertEquals(InvitationStatus.ACCEPTED, invitation.getStatus()); +// verify(teamInvitationRepository, times(1)).save(invitation); +// verify(teamUserRelationRepository, times(1)).save(any(TeamUserRelation.class)); +// } +// +// @Test +// void acceptTeamInvitation_InvitationNotFound_ThrowsException() { +// // given +// when(teamInvitationRepository.findById(1L)).thenReturn(Optional.empty()); +// +// // when & then +// BusinessException exception = assertThrows(BusinessException.class, () -> { +// teamService.acceptTeamInvitation(1L, 1L); +// }); +// +// assertEquals(ErrorCode.INVITATION_NOT_FOUND, exception.getErrorCode()); +// verify(teamInvitationRepository, never()).save(any()); +// } +// +// @Test +// void acceptTeamInvitation_AccessDenied_ThrowsException() { +// // given +// User anotherUser = User.builder().id(2L).nickname("AnotherUser").build(); +// TeamInvitation invitation = new TeamInvitation(team, anotherUser); +// +// when(teamInvitationRepository.findById(1L)).thenReturn(Optional.of(invitation)); +// +// // when & then +// BusinessException exception = assertThrows(BusinessException.class, () -> { +// teamService.acceptTeamInvitation(1L, 1L); // 유저 ID가 초대 유저와 일치하지 않음 +// }); +// +// assertEquals(ErrorCode.ACCESS_DENIED, exception.getErrorCode()); +// verify(teamInvitationRepository, never()).save(any()); +// } +// +// @Test +// void acceptTeamInvitation_InvitationAlreadyProcessed_ThrowsException() { +// // given +// TeamInvitation invitation = new TeamInvitation(team, user); +// invitation.accept(); // 초대가 이미 처리됨 +// +// when(teamInvitationRepository.findById(1L)).thenReturn(Optional.of(invitation)); +// +// // when & then +// BusinessException exception = assertThrows(BusinessException.class, () -> { +// teamService.acceptTeamInvitation(1L, 1L); +// }); +// +// assertEquals(ErrorCode.INVITATION_ALREADY_PROCESSED, exception.getErrorCode()); +// verify(teamInvitationRepository, never()).save(any()); +// } +// +// @Test +// void rejectTeamInvitation() { +// // given +// TeamInvitation invitation = new TeamInvitation(team, user); +// +// when(teamInvitationRepository.findById(1L)).thenReturn(Optional.of(invitation)); +// +// // when +// teamService.rejectTeamInvitation(1L, 1L); +// +// // then +// assertEquals(InvitationStatus.REJECTED, invitation.getStatus()); +// verify(teamInvitationRepository, times(1)).save(invitation); +// } +// +// @Test +// void rejectTeamInvitation_InvitationNotFound_ThrowsException() { +// // given +// when(teamInvitationRepository.findById(1L)).thenReturn(Optional.empty()); +// +// // when & then +// BusinessException exception = assertThrows(BusinessException.class, () -> { +// teamService.rejectTeamInvitation(1L, 1L); +// }); +// +// assertEquals(ErrorCode.INVITATION_NOT_FOUND, exception.getErrorCode()); +// verify(teamInvitationRepository, never()).save(any()); +// } +// +// @Test +// void rejectTeamInvitation_AccessDenied_ThrowsException() { +// // given +// User anotherUser = User.builder().id(2L).nickname("AnotherUser").build(); +// TeamInvitation invitation = new TeamInvitation(team, anotherUser); +// +// when(teamInvitationRepository.findById(1L)).thenReturn(Optional.of(invitation)); +// +// // when & then +// BusinessException exception = assertThrows(BusinessException.class, () -> { +// teamService.rejectTeamInvitation(1L, 1L); // 유저 ID가 초대 유저와 일치하지 않음 +// }); +// +// assertEquals(ErrorCode.ACCESS_DENIED, exception.getErrorCode()); +// verify(teamInvitationRepository, never()).save(any()); +// } +// +// @Test +// void rejectTeamInvitation_InvitationAlreadyProcessed_ThrowsException() { +// // given +// TeamInvitation invitation = new TeamInvitation(team, user); +// invitation.reject(); // 초대가 이미 처리됨 +// +// when(teamInvitationRepository.findById(1L)).thenReturn(Optional.of(invitation)); +// +// // when & then +// BusinessException exception = assertThrows(BusinessException.class, () -> { +// teamService.rejectTeamInvitation(1L, 1L); +// }); +// +// assertEquals(ErrorCode.INVITATION_ALREADY_PROCESSED, exception.getErrorCode()); +// verify(teamInvitationRepository, never()).save(any()); +// } +// +// @Test +// void getTeamMembers() { +// // given +// when(teamRepository.findById(1L)).thenReturn(Optional.of(team)); +// when(userRepository.findById(1L)).thenReturn(Optional.of(user)); +// when(teamUserRelationRepository.findByTeamAndUser(team, user)).thenReturn(Optional.of(new TeamUserRelation(team, user, UserTeamRole.ADMIN))); +// +// User member1 = User.builder().id(2L).nickname("Member1").build(); +// User member2 = User.builder().id(3L).nickname("Member2").build(); +// +// TeamUserRelation relation1 = new TeamUserRelation(team, member1, UserTeamRole.MEMBER); +// TeamUserRelation relation2 = new TeamUserRelation(team, member2, UserTeamRole.MEMBER); +// +// when(teamUserRelationRepository.findAllByTeam(team)).thenReturn(Arrays.asList(relation1, relation2)); +// +// // when +// List teamMembers = teamService.getTeamMembers(1L, 1L); +// +// // then +// assertNotNull(teamMembers); +// assertEquals(2, teamMembers.size()); +// assertEquals("Member1", teamMembers.get(0).getNickname()); +// assertEquals("Member2", teamMembers.get(1).getNickname()); +// +// verify(teamRepository, times(1)).findById(1L); +// verify(userRepository, times(1)).findById(1L); +// verify(teamUserRelationRepository, times(1)).findAllByTeam(team); +// } +// +// @Test +// void getTeamMembers_TeamNotFound_ThrowsException() { +// // given +// when(teamRepository.findById(1L)).thenReturn(Optional.empty()); +// +// // when & then +// BusinessException exception = assertThrows(BusinessException.class, () -> { +// teamService.getTeamMembers(1L, 1L); +// }); +// +// assertEquals(ErrorCode.TEAM_NOT_FOUND, exception.getErrorCode()); +// verify(teamUserRelationRepository, never()).findAllByTeam(any()); +// } +// +// @Test +// void getTeamMembers_UserNotFound_ThrowsException() { +// // given +// when(teamRepository.findById(1L)).thenReturn(Optional.of(team)); +// when(userRepository.findById(1L)).thenReturn(Optional.empty()); +// +// // when & then +// BusinessException exception = assertThrows(BusinessException.class, () -> { +// teamService.getTeamMembers(1L, 1L); +// }); +// +// assertEquals(ErrorCode.USER_NOT_FOUND, exception.getErrorCode()); +// verify(teamUserRelationRepository, never()).findAllByTeam(any()); +// } +// +// @Test +// void getTeamMembers_AccessDenied_ThrowsException() { +// // given +// when(teamRepository.findById(1L)).thenReturn(Optional.of(team)); +// when(userRepository.findById(1L)).thenReturn(Optional.of(user)); +// when(teamUserRelationRepository.findByTeamAndUser(team, user)).thenReturn(Optional.empty()); +// +// // when & then +// BusinessException exception = assertThrows(BusinessException.class, () -> { +// teamService.getTeamMembers(1L, 1L); +// }); +// +// assertEquals(ErrorCode.TEAM_MEMBER_NOT_FOUND, exception.getErrorCode()); +// verify(teamUserRelationRepository, never()).findAllByTeam(any()); +// } +// +// @Test +// void promoteUserToAdmin() { +// // given +// when(teamRepository.findById(1L)).thenReturn(Optional.of(team)); +// when(userRepository.findById(1L)).thenReturn(Optional.of(user)); // Admin user +// when(userRepository.findById(2L)).thenReturn(Optional.of(user2)); // User to be promoted +// when(teamUserRelationRepository.findByTeamAndUser(team, user)).thenReturn(Optional.of(new TeamUserRelation(team, user, UserTeamRole.ADMIN))); +// when(teamUserRelationRepository.findByTeamAndUser(team, user2)).thenReturn(Optional.of(new TeamUserRelation(team, user2, UserTeamRole.MEMBER))); +// +// // when +// teamService.promoteUserToAdmin(1L, 2L, 1L); +// +// // then +// verify(teamUserRelationRepository, times(1)).save(any(TeamUserRelation.class)); +// } +// +// @Test +// void promoteUserToAdmin_TeamNotFound_ThrowsException() { +// // given +// when(teamRepository.findById(1L)).thenReturn(Optional.empty()); +// +// // when & then +// BusinessException exception = assertThrows(BusinessException.class, () -> { +// teamService.promoteUserToAdmin(1L, 2L, 1L); +// }); +// +// assertEquals(ErrorCode.TEAM_NOT_FOUND, exception.getErrorCode()); +// verify(teamUserRelationRepository, never()).save(any()); +// } +// +// @Test +// void promoteUserToAdmin_UserNotFound_ThrowsException() { +// // given +// when(teamRepository.findById(1L)).thenReturn(Optional.of(team)); +// when(userRepository.findById(1L)).thenReturn(Optional.of(user)); // Admin user +// when(userRepository.findById(2L)).thenReturn(Optional.empty()); // User to be promoted +// +// // when & then +// BusinessException exception = assertThrows(BusinessException.class, () -> { +// teamService.promoteUserToAdmin(1L, 2L, 1L); +// }); +// +// assertEquals(ErrorCode.USER_NOT_FOUND, exception.getErrorCode()); +// verify(teamUserRelationRepository, never()).save(any()); +// } +// +// @Test +// void promoteUserToAdmin_AccessDenied_ThrowsException() { +// // given +// when(teamRepository.findById(1L)).thenReturn(Optional.of(team)); +// when(userRepository.findById(1L)).thenReturn(Optional.of(user)); // Admin user +// when(userRepository.findById(2L)).thenReturn(Optional.of(user2)); // User to be promoted +// when(teamUserRelationRepository.findByTeamAndUser(team, user)).thenReturn(Optional.empty()); // Not an admin +// +// // when & then +// BusinessException exception = assertThrows(BusinessException.class, () -> { +// teamService.promoteUserToAdmin(1L, 2L, 1L); +// }); +// +// assertEquals(ErrorCode.ACCESS_DENIED, exception.getErrorCode()); +// verify(teamUserRelationRepository, never()).save(any()); +// } +//} \ No newline at end of file From 11152ccc9278f110ce9c6d3a944e84ef07d328a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=84=B8=EC=A7=84?= Date: Sat, 12 Oct 2024 18:34:52 +0900 Subject: [PATCH 07/46] =?UTF-8?q?refactor:=20=EC=B9=9C=EA=B5=AC=EC=9A=94?= =?UTF-8?q?=EC=B2=AD=EC=9D=84=20=EC=9D=B4=EB=AF=B8=20=EB=B3=B4=EB=83=88?= =?UTF-8?q?=EC=9D=84=20=EA=B2=BD=EC=9A=B0=20=EC=98=88=EC=99=B8=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/splanet/splanet/core/exception/ErrorCode.java | 1 + .../friendRequest/repository/FriendRequestRepository.java | 4 +++- .../splanet/friendRequest/service/FriendRequestService.java | 6 ++++++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/splanet/splanet/core/exception/ErrorCode.java b/src/main/java/com/splanet/splanet/core/exception/ErrorCode.java index fe7bde3a..941f7bd3 100644 --- a/src/main/java/com/splanet/splanet/core/exception/ErrorCode.java +++ b/src/main/java/com/splanet/splanet/core/exception/ErrorCode.java @@ -40,6 +40,7 @@ public enum ErrorCode { // friend FRIEND_REQUEST_NOT_FOUND("받은 친구 요청이 없습니다.", HttpStatus.NOT_FOUND), FRIEND_ALREADY_EXISTS("이미 친구 목록에 있습니다.", HttpStatus.BAD_REQUEST), + FRIEND_REQUEST_ALREADY_SENT("이미 요청을 보냈습니다.",HttpStatus.BAD_REQUEST), FRIEND_REQUEST_ALREADY_ACCEPTED_OR_REJECTED("이미 수락하거나 거절한 사용자 입니다.", HttpStatus.BAD_REQUEST), SELF_FRIEND_REQUEST_NOT_ALLOWED("본인에게 친구요청을 보낼 수 없습니다.", HttpStatus.BAD_REQUEST); diff --git a/src/main/java/com/splanet/splanet/friendRequest/repository/FriendRequestRepository.java b/src/main/java/com/splanet/splanet/friendRequest/repository/FriendRequestRepository.java index ab73a070..196405c8 100644 --- a/src/main/java/com/splanet/splanet/friendRequest/repository/FriendRequestRepository.java +++ b/src/main/java/com/splanet/splanet/friendRequest/repository/FriendRequestRepository.java @@ -5,7 +5,6 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; -import org.w3c.dom.stylesheets.LinkStyle; import java.util.List; @@ -16,4 +15,7 @@ public interface FriendRequestRepository extends JpaRepository findByRequesterId(@Param("userId") Long userId); + + @Query("SELECT fr FROM FriendRequest fr WHERE fr.receiver.id = :receiverId AND fr.requester.id = :requesterId AND fr.status = :status") + List findPendingRequestsByReceiverId(@Param("receiverId") Long receiverId, @Param("requesterId") Long requesterId, @Param("status") FriendRequest.Status status); } \ No newline at end of file diff --git a/src/main/java/com/splanet/splanet/friendRequest/service/FriendRequestService.java b/src/main/java/com/splanet/splanet/friendRequest/service/FriendRequestService.java index 9baecc89..c93d0b50 100644 --- a/src/main/java/com/splanet/splanet/friendRequest/service/FriendRequestService.java +++ b/src/main/java/com/splanet/splanet/friendRequest/service/FriendRequestService.java @@ -40,6 +40,12 @@ public void sendFriendRequest(Long userId, Long receiverId) { throw new BusinessException(ErrorCode.FRIEND_ALREADY_EXISTS); } + // 이미 보낸 요청이 있는지 확인 + List existingRequests = friendRequestRepository.findPendingRequestsByReceiverId(userId, receiverId, FriendRequest.Status.PENDING); + if (!existingRequests.isEmpty()) { + throw new BusinessException(ErrorCode.FRIEND_REQUEST_ALREADY_SENT); + } + User receiver = userRepository.findById(receiverId) .orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND)); User requester = userRepository.findById(userId) From 5c6c1623a6fa403c895c780bdace750d9c26d640 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=84=B8=EC=A7=84?= Date: Sat, 12 Oct 2024 18:35:09 +0900 Subject: [PATCH 08/46] =?UTF-8?q?test:=20=EC=B9=9C=EA=B5=ACapi=20=EC=84=9C?= =?UTF-8?q?=EB=B9=84=EC=8A=A4=EB=A1=9C=EC=A7=81=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/FriendRequestServiceTest.java | 207 ++++++++++++++++++ 1 file changed, 207 insertions(+) create mode 100644 src/test/java/com/splanet/splanet/friendRequest/service/FriendRequestServiceTest.java diff --git a/src/test/java/com/splanet/splanet/friendRequest/service/FriendRequestServiceTest.java b/src/test/java/com/splanet/splanet/friendRequest/service/FriendRequestServiceTest.java new file mode 100644 index 00000000..e24517ef --- /dev/null +++ b/src/test/java/com/splanet/splanet/friendRequest/service/FriendRequestServiceTest.java @@ -0,0 +1,207 @@ +package com.splanet.splanet.friendRequest.service; + +import com.splanet.splanet.core.exception.BusinessException; +import com.splanet.splanet.core.exception.ErrorCode; +import com.splanet.splanet.friend.entity.Friend; +import com.splanet.splanet.friend.repository.FriendRepository; +import com.splanet.splanet.friendRequest.dto.ReceivedFriendRequestResponse; +import com.splanet.splanet.friendRequest.dto.SentFriendRequestResponse; +import com.splanet.splanet.friendRequest.entity.FriendRequest; +import com.splanet.splanet.friendRequest.repository.FriendRequestRepository; +import com.splanet.splanet.user.entity.User; +import com.splanet.splanet.user.repository.UserRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +class FriendRequestServiceTest { + + @Mock + private FriendRequestRepository friendRequestRepository; + + @Mock + private FriendRepository friendRepository; + + @Mock + private UserRepository userRepository; + + @InjectMocks + private FriendRequestService friendRequestService; + + private User requester; + private User receiver; + private FriendRequest friendRequest; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + + // 유저 객체 생성 + requester = User.builder() + .id(1L) + .nickname("요청자") + .profileImage("requester.png") + .build(); + + receiver = User.builder() + .id(2L) + .nickname("수락자") + .profileImage("receiver.png") + .build(); + + // 친구 요청 객체 생성 + friendRequest = FriendRequest.builder() + .id(1L) + .requester(requester) + .receiver(receiver) + .status(FriendRequest.Status.PENDING) + .build(); + } + + @Test + void 친구요청전송_성공() { + Long userId = requester.getId(); + Long receiverId = receiver.getId(); + + when(userRepository.findById(receiverId)).thenReturn(Optional.of(receiver)); + when(userRepository.findById(userId)).thenReturn(Optional.of(requester)); + when(friendRepository.existsByUserIdAndFriendId(userId, receiverId)).thenReturn(false); + + friendRequestService.sendFriendRequest(userId, receiverId); + + verify(friendRequestRepository, times(1)).save(any(FriendRequest.class)); + } + + @Test + void 친구요청전송_본인에게요청() { + Long userId = requester.getId(); + Long receiverId = requester.getId(); + + BusinessException exception = assertThrows(BusinessException.class, () -> + friendRequestService.sendFriendRequest(userId, receiverId) + ); + + assertEquals(ErrorCode.SELF_FRIEND_REQUEST_NOT_ALLOWED, exception.getErrorCode()); + } + + @Test + void 친구요청수락_성공() { + Long requestId = friendRequest.getId(); + + when(friendRequestRepository.findById(requestId)).thenReturn(Optional.of(friendRequest)); + + // 직접 상태를 설정 + friendRequest.setStatus(FriendRequest.Status.PENDING); + + ReceivedFriendRequestResponse response = friendRequestService.acceptFriendRequest(requestId); + + assertEquals(requester.getId(), response.requesterId()); // 요청자의 ID가 맞는지 확인 + verify(friendRequestRepository, times(1)).save(friendRequest); + verify(friendRepository, times(2)).save(any(Friend.class)); + } + + @Test + void 친구요청수락_요청없음() { + Long requestId = 1L; + + when(friendRequestRepository.findById(requestId)).thenReturn(Optional.empty()); + + BusinessException exception = assertThrows(BusinessException.class, () -> + friendRequestService.acceptFriendRequest(requestId) + ); + + assertEquals(ErrorCode.FRIEND_REQUEST_NOT_FOUND, exception.getErrorCode()); + } + + @Test + void 친구요청거절_성공() { + Long requestId = friendRequest.getId(); + + when(friendRequestRepository.findById(requestId)).thenReturn(Optional.of(friendRequest)); + + // 직접 상태를 설정 + friendRequest.setStatus(FriendRequest.Status.PENDING); + + ReceivedFriendRequestResponse response = friendRequestService.rejectFriendRequest(requestId); + + assertEquals(requester.getId(), response.requesterId()); // 요청자의 ID가 맞는지 확인 + verify(friendRequestRepository, times(1)).save(friendRequest); + } + + @Test + void 친구요청목록조회_받은요청() { + Long userId = receiver.getId(); + + when(friendRequestRepository.findByReceiverId(userId)).thenReturn(Arrays.asList(friendRequest)); + + List responses = friendRequestService.getReceivedFriendRequests(userId); + + assertEquals(1, responses.size()); + assertEquals(requester.getId(), responses.get(0).requesterId()); + } + + @Test + void 친구요청목록조회_보낸요청() { + Long userId = requester.getId(); + + when(friendRequestRepository.findByRequesterId(userId)).thenReturn(Arrays.asList(friendRequest)); + + List responses = friendRequestService.getSentFriendRequests(userId); + + assertEquals(1, responses.size()); + assertEquals(receiver.getId(), responses.get(0).receiverId()); + } + + @Test + void 친구요청전송_수신자존재하지않음() { + Long userId = requester.getId(); + Long receiverId = 999L; // 존재하지 않는 ID + + when(userRepository.findById(receiverId)).thenReturn(Optional.empty()); + + BusinessException exception = assertThrows(BusinessException.class, () -> + friendRequestService.sendFriendRequest(userId, receiverId) + ); + + assertEquals(ErrorCode.USER_NOT_FOUND, exception.getErrorCode()); + } + + @Test + void 친구요청수락_이미수락된요청() { + Long requestId = friendRequest.getId(); + friendRequest.setStatus(FriendRequest.Status.ACCEPTED); // 상태를 수락으로 설정 + + when(friendRequestRepository.findById(requestId)).thenReturn(Optional.of(friendRequest)); + + BusinessException exception = assertThrows(BusinessException.class, () -> + friendRequestService.acceptFriendRequest(requestId) + ); + + assertEquals(ErrorCode.FRIEND_REQUEST_ALREADY_ACCEPTED_OR_REJECTED, exception.getErrorCode()); + } + + @Test + void 친구요청수락_이미거절된요청() { + Long requestId = friendRequest.getId(); + friendRequest.setStatus(FriendRequest.Status.REJECTED); // 상태를 거절로 설정 + + when(friendRequestRepository.findById(requestId)).thenReturn(Optional.of(friendRequest)); + + BusinessException exception = assertThrows(BusinessException.class, () -> + friendRequestService.acceptFriendRequest(requestId) + ); + + assertEquals(ErrorCode.FRIEND_REQUEST_ALREADY_ACCEPTED_OR_REJECTED, exception.getErrorCode()); + } +} \ No newline at end of file From 193efaac80aac4c60e134724325d8bea51bf5348 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=84=B8=EC=A7=84?= Date: Sat, 12 Oct 2024 18:44:05 +0900 Subject: [PATCH 09/46] =?UTF-8?q?refactor:=20=EC=A7=84=EC=A7=9C!=20?= =?UTF-8?q?=EC=9A=94=EC=B2=AD=20=EB=98=90=20=EB=AA=BB=EB=B3=B4=EB=83=84!?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../splanet/friendRequest/service/FriendRequestService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/splanet/splanet/friendRequest/service/FriendRequestService.java b/src/main/java/com/splanet/splanet/friendRequest/service/FriendRequestService.java index c93d0b50..d464fb76 100644 --- a/src/main/java/com/splanet/splanet/friendRequest/service/FriendRequestService.java +++ b/src/main/java/com/splanet/splanet/friendRequest/service/FriendRequestService.java @@ -41,7 +41,7 @@ public void sendFriendRequest(Long userId, Long receiverId) { } // 이미 보낸 요청이 있는지 확인 - List existingRequests = friendRequestRepository.findPendingRequestsByReceiverId(userId, receiverId, FriendRequest.Status.PENDING); + List existingRequests = friendRequestRepository.findPendingRequestsByReceiverId(receiverId, userId, FriendRequest.Status.PENDING); if (!existingRequests.isEmpty()) { throw new BusinessException(ErrorCode.FRIEND_REQUEST_ALREADY_SENT); } From 9cd8d54fb530dfd7d389991c4e3b9ca49f2660af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=84=B8=EC=A7=84?= Date: Mon, 14 Oct 2024 00:59:59 +0900 Subject: [PATCH 10/46] =?UTF-8?q?refactor:=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=EB=B0=98=EC=98=81=ED=95=98=EC=97=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../splanet/core/exception/ErrorCode.java | 2 + .../splanet/friend/controller/FriendApi.java | 10 +++- .../friend/controller/FriendController.java | 19 ++++--- .../friend/repository/FriendRepository.java | 9 +++- .../splanet/friend/service/FriendService.java | 36 +++++++++++-- .../controller/FriendRequestApi.java | 4 +- .../controller/FriendRequestController.java | 10 ++-- .../friendRequest/entity/FriendRequest.java | 12 ++++- .../service/FriendRequestService.java | 50 +++++++++++++++---- 9 files changed, 122 insertions(+), 30 deletions(-) diff --git a/src/main/java/com/splanet/splanet/core/exception/ErrorCode.java b/src/main/java/com/splanet/splanet/core/exception/ErrorCode.java index 941f7bd3..9431b1e5 100644 --- a/src/main/java/com/splanet/splanet/core/exception/ErrorCode.java +++ b/src/main/java/com/splanet/splanet/core/exception/ErrorCode.java @@ -38,9 +38,11 @@ public enum ErrorCode { // friend + FRIEND_NOT_FOUND("친구가 아닙니다.",HttpStatus.NOT_FOUND), FRIEND_REQUEST_NOT_FOUND("받은 친구 요청이 없습니다.", HttpStatus.NOT_FOUND), FRIEND_ALREADY_EXISTS("이미 친구 목록에 있습니다.", HttpStatus.BAD_REQUEST), FRIEND_REQUEST_ALREADY_SENT("이미 요청을 보냈습니다.",HttpStatus.BAD_REQUEST), + FRIEND_REQUEST_NOT_RECEIVER("본인이 보낸 요청은 수락하거나 거절할 수 없습니다.", HttpStatus.BAD_REQUEST), FRIEND_REQUEST_ALREADY_ACCEPTED_OR_REJECTED("이미 수락하거나 거절한 사용자 입니다.", HttpStatus.BAD_REQUEST), SELF_FRIEND_REQUEST_NOT_ALLOWED("본인에게 친구요청을 보낼 수 없습니다.", HttpStatus.BAD_REQUEST); diff --git a/src/main/java/com/splanet/splanet/friend/controller/FriendApi.java b/src/main/java/com/splanet/splanet/friend/controller/FriendApi.java index 6b6c8902..d3a2114e 100644 --- a/src/main/java/com/splanet/splanet/friend/controller/FriendApi.java +++ b/src/main/java/com/splanet/splanet/friend/controller/FriendApi.java @@ -9,11 +9,13 @@ import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import java.util.List; +import java.util.Map; @RequestMapping("/api/friends") @Tag(name = "Friend", description = "친구 관련 API") @@ -37,6 +39,12 @@ ResponseEntity> getFriends( @ApiResponse(responseCode = "404", description = "친구를 찾을 수 없습니다.") }) ResponseEntity> getFriendPlan( - @Parameter(description = "JWT 인증으로 전달된 친구 ID", required = true) @PathVariable("friend_id") Long friendId, + @Parameter(description = "조회할 친구 ID", required = true) @PathVariable("friendId") Long friendId, + @Parameter(description = "JWT 인증으로 전달된 사용자 ID", required = true) @AuthenticationPrincipal Long userId); + + @DeleteMapping("/{friendId}") + @Operation(summary = "친구 삭제하기", description = "친구 목록에서 삭제합니다.") + ResponseEntity> unfriend( + @Parameter(description = "삭제할 친구 ID", required = true) @PathVariable("friendId") Long friendId, @Parameter(description = "JWT 인증으로 전달된 사용자 ID", required = true) @AuthenticationPrincipal Long userId); } \ No newline at end of file diff --git a/src/main/java/com/splanet/splanet/friend/controller/FriendController.java b/src/main/java/com/splanet/splanet/friend/controller/FriendController.java index 6f0c62ee..ebbbccc7 100644 --- a/src/main/java/com/splanet/splanet/friend/controller/FriendController.java +++ b/src/main/java/com/splanet/splanet/friend/controller/FriendController.java @@ -11,18 +11,15 @@ import org.springframework.web.bind.annotation.RestController; import java.util.List; +import java.util.Map; @RestController public class FriendController implements FriendApi { private final FriendService friendService; - private final UserRepository userRepository; - private final PlanRepository planRepository; - public FriendController(FriendService friendService, UserRepository userRepository, PlanRepository planRepository) { - this.friendService = friendService; - this.userRepository = userRepository; - this.planRepository = planRepository; + public FriendController(FriendService friendService) { + this.friendService = friendService;; } @Override @@ -37,4 +34,12 @@ public ResponseEntity> getFriendPlan( @AuthenticationPrincipal Long userId) { return friendService.getFriendPlan(friendId, userId); } -} + + @Override + public ResponseEntity> unfriend( + @PathVariable Long friendId, + @AuthenticationPrincipal Long userId) { + ResponseEntity> responseEntity = friendService.unfriend(friendId, userId); + return responseEntity; + } +} \ No newline at end of file diff --git a/src/main/java/com/splanet/splanet/friend/repository/FriendRepository.java b/src/main/java/com/splanet/splanet/friend/repository/FriendRepository.java index 96b8677b..03b87d14 100644 --- a/src/main/java/com/splanet/splanet/friend/repository/FriendRepository.java +++ b/src/main/java/com/splanet/splanet/friend/repository/FriendRepository.java @@ -2,13 +2,20 @@ import com.splanet.splanet.friend.entity.Friend; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; import java.util.List; -import java.util.Optional; + @Repository public interface FriendRepository extends JpaRepository { List findByUserId(Long userId); boolean existsByUserIdAndFriendId(Long userId, Long friendId); + + @Modifying + @Query("DELETE FROM Friend f WHERE f.user.id = :requesterId AND f.friend.id = :receiverId") + void deleteByRequesterIdAndReceiverId(@Param("requesterId") Long requesterId, @Param("receiverId") Long receiverId); } \ No newline at end of file diff --git a/src/main/java/com/splanet/splanet/friend/service/FriendService.java b/src/main/java/com/splanet/splanet/friend/service/FriendService.java index 8efafa41..283da25a 100644 --- a/src/main/java/com/splanet/splanet/friend/service/FriendService.java +++ b/src/main/java/com/splanet/splanet/friend/service/FriendService.java @@ -5,6 +5,8 @@ import com.splanet.splanet.friend.dto.FriendResponse; import com.splanet.splanet.friend.entity.Friend; import com.splanet.splanet.friend.repository.FriendRepository; +import com.splanet.splanet.friendRequest.entity.FriendRequest; +import com.splanet.splanet.friendRequest.repository.FriendRequestRepository; import com.splanet.splanet.plan.dto.PlanResponseDto; import com.splanet.splanet.plan.entity.Plan; import com.splanet.splanet.plan.repository.PlanRepository; @@ -13,20 +15,22 @@ import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; +import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; @Service public class FriendService { private final FriendRepository friendRepository; - private final UserRepository userRepository; private final PlanRepository planRepository; + private final FriendRequestRepository friendRequestRepository; - public FriendService(FriendRepository friendRepository, UserRepository userRepository, PlanRepository planRepository) { + public FriendService(FriendRepository friendRepository, PlanRepository planRepository, FriendRequestRepository friendRequestRepository) { this.friendRepository = friendRepository; - this.userRepository = userRepository; this.planRepository = planRepository; + this.friendRequestRepository = friendRequestRepository; } // 친구 목록 조회 @@ -48,10 +52,18 @@ public List getFriends(Long userId) { // 친구의 공개 플랜 조회 public ResponseEntity> getFriendPlan(Long friendId, Long userId) { + // 친구 목록에 friendId가 있는지 확인 + boolean isFriend = friendRepository.existsByUserIdAndFriendId(userId, friendId); + + if (!isFriend) { + throw new BusinessException(ErrorCode.FRIEND_NOT_FOUND); + } + List publicPlans = planRepository.findAllByUserIdAndAccessibility(friendId, true); + // 공개된 플랜이 없을 경우, 빈 목록 반환 if (publicPlans.isEmpty()) { - throw new BusinessException(ErrorCode.PLAN_NOT_FOUND); + return ResponseEntity.ok(Collections.emptyList()); } List planResponseDtos = publicPlans.stream() @@ -70,4 +82,20 @@ public ResponseEntity> getFriendPlan(Long friendId, Long u return ResponseEntity.ok(planResponseDtos); } + + // 친구 삭제(취소)하기 + public ResponseEntity> unfriend(Long friendId, Long userId) { + if (!friendRepository.existsByUserIdAndFriendId(userId, friendId)) { + throw new BusinessException(ErrorCode.FRIEND_NOT_FOUND); + } + + friendRepository.deleteByRequesterIdAndReceiverId(userId, friendId); + + List pendingRequests = friendRequestRepository.findPendingRequestsByReceiverId(userId, friendId, FriendRequest.Status.PENDING); + for (FriendRequest request : pendingRequests) { + friendRequestRepository.delete(request); + } + + return ResponseEntity.ok(Map.of("message", "친구 맺기 취소되었습니다!")); + } } \ No newline at end of file diff --git a/src/main/java/com/splanet/splanet/friendRequest/controller/FriendRequestApi.java b/src/main/java/com/splanet/splanet/friendRequest/controller/FriendRequestApi.java index 943c9e3d..b31c72d5 100644 --- a/src/main/java/com/splanet/splanet/friendRequest/controller/FriendRequestApi.java +++ b/src/main/java/com/splanet/splanet/friendRequest/controller/FriendRequestApi.java @@ -38,7 +38,7 @@ ResponseEntity sendFriendRequest( @ApiResponse(responseCode = "401", description = "인증되지 않은 사용자입니다."), @ApiResponse(responseCode = "404", description = "친구 요청을 찾을 수 없습니다.") }) - ResponseEntity acceptFriendRequest( + ResponseEntity acceptFriendRequest(@AuthenticationPrincipal Long userId, @Parameter(description = "친구 요청 ID", required = true) @PathVariable Long requestId); @PostMapping("/{requestId}/reject") @@ -50,7 +50,7 @@ ResponseEntity acceptFriendRequest( @ApiResponse(responseCode = "404", description = "친구 요청을 찾을 수 없습니다.") }) ResponseEntity rejectFriendRequest( - @Parameter(description = "친구 요청 ID", required = true) @PathVariable Long requestId); + @Parameter(description = "친구 요청 ID", required = true) @PathVariable Long requestId, @AuthenticationPrincipal Long userId); @GetMapping("/received") @Operation(summary = "친구 요청 목록 조회 (받은 요청)", description = "사용자가 받은 친구 요청 목록을 조회합니다.") diff --git a/src/main/java/com/splanet/splanet/friendRequest/controller/FriendRequestController.java b/src/main/java/com/splanet/splanet/friendRequest/controller/FriendRequestController.java index 09b7a665..a952084b 100644 --- a/src/main/java/com/splanet/splanet/friendRequest/controller/FriendRequestController.java +++ b/src/main/java/com/splanet/splanet/friendRequest/controller/FriendRequestController.java @@ -36,14 +36,16 @@ public ResponseEntity sendFriendRequest(@AuthenticationPrincipa // 친구 요청 수락 @Override - public ResponseEntity acceptFriendRequest(@PathVariable Long requestId) { - return ResponseEntity.ok(friendRequestService.acceptFriendRequest(requestId)); + public ResponseEntity acceptFriendRequest(@AuthenticationPrincipal Long userId, + @PathVariable Long requestId) { + ReceivedFriendRequestResponse response = friendRequestService.acceptFriendRequest(requestId, userId); + return ResponseEntity.ok(response); } // 친구 요청 거절 @Override - public ResponseEntity rejectFriendRequest(@PathVariable Long requestId) { - return ResponseEntity.ok(friendRequestService.rejectFriendRequest(requestId)); + public ResponseEntity rejectFriendRequest(@PathVariable Long requestId, @AuthenticationPrincipal Long userId) { + return ResponseEntity.ok(friendRequestService.rejectFriendRequest(requestId, userId)); } // 친구 요청 목록 조회(받은 요청) diff --git a/src/main/java/com/splanet/splanet/friendRequest/entity/FriendRequest.java b/src/main/java/com/splanet/splanet/friendRequest/entity/FriendRequest.java index ce2c1115..f4bb62a5 100644 --- a/src/main/java/com/splanet/splanet/friendRequest/entity/FriendRequest.java +++ b/src/main/java/com/splanet/splanet/friendRequest/entity/FriendRequest.java @@ -1,6 +1,7 @@ package com.splanet.splanet.friendRequest.entity; import com.splanet.splanet.core.BaseEntity; +import com.splanet.splanet.friend.entity.Friend; import com.splanet.splanet.user.entity.User; import jakarta.persistence.*; import lombok.*; @@ -14,11 +15,11 @@ @Entity public class FriendRequest extends BaseEntity { - @ManyToOne + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "requester_id", nullable = false) private User requester; - @ManyToOne + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "receiver_id", nullable = false) private User receiver; @@ -37,4 +38,11 @@ public enum Status { ACCEPTED, REJECTED } + + public Friend accept() { + return Friend.builder() + .user(requester) + .friend(receiver) + .build(); + } } \ No newline at end of file diff --git a/src/main/java/com/splanet/splanet/friendRequest/service/FriendRequestService.java b/src/main/java/com/splanet/splanet/friendRequest/service/FriendRequestService.java index d464fb76..d9c5ff80 100644 --- a/src/main/java/com/splanet/splanet/friendRequest/service/FriendRequestService.java +++ b/src/main/java/com/splanet/splanet/friendRequest/service/FriendRequestService.java @@ -12,6 +12,7 @@ import com.splanet.splanet.user.repository.UserRepository; import org.springframework.stereotype.Service; +import java.time.LocalDateTime; import java.util.List; import java.util.stream.Collectors; @@ -61,7 +62,7 @@ public void sendFriendRequest(Long userId, Long receiverId) { } // 친구 요청 수락 - public ReceivedFriendRequestResponse acceptFriendRequest(Long requestId) { + public ReceivedFriendRequestResponse acceptFriendRequest(Long requestId, Long userId) { FriendRequest friendRequest = friendRequestRepository.findById(requestId) .orElseThrow(() -> new BusinessException(ErrorCode.FRIEND_REQUEST_NOT_FOUND)); @@ -69,14 +70,33 @@ public ReceivedFriendRequestResponse acceptFriendRequest(Long requestId) { throw new BusinessException(ErrorCode.FRIEND_REQUEST_ALREADY_ACCEPTED_OR_REJECTED); } - friendRequest.setStatus(FriendRequest.Status.ACCEPTED); - friendRequestRepository.save(friendRequest); + if (!friendRequest.getReceiver().getId().equals(userId)) { + throw new BusinessException(ErrorCode.FRIEND_REQUEST_NOT_RECEIVER); + } + + FriendRequest updatedFriendRequest = FriendRequest.builder() + .id(friendRequest.getId()) + .requester(friendRequest.getRequester()) + .receiver(friendRequest.getReceiver()) + .status(FriendRequest.Status.ACCEPTED) + .createdAt(friendRequest.getCreatedAt()) + .updatedAt(LocalDateTime.now()) + .build(); + + friendRequestRepository.save(updatedFriendRequest); User requester = friendRequest.getRequester(); User receiver = friendRequest.getReceiver(); - Friend friend1 = new Friend(requester, receiver); // 요청한 사람 -> 수락한 사람 - Friend friend2 = new Friend(receiver, requester); // 수락한 사람 -> 요청한 사람 + Friend friend1 = Friend.builder() + .user(requester) + .friend(receiver) + .build(); + + Friend friend2 = Friend.builder() + .user(receiver) + .friend(requester) + .build(); friendRepository.save(friend1); friendRepository.save(friend2); @@ -91,7 +111,7 @@ public ReceivedFriendRequestResponse acceptFriendRequest(Long requestId) { } // 친구 요청 거절 - public ReceivedFriendRequestResponse rejectFriendRequest(Long requestId) { + public ReceivedFriendRequestResponse rejectFriendRequest(Long requestId, Long userId) { FriendRequest friendRequest = friendRequestRepository.findById(requestId) .orElseThrow(() -> new BusinessException(ErrorCode.FRIEND_REQUEST_NOT_FOUND)); @@ -99,8 +119,20 @@ public ReceivedFriendRequestResponse rejectFriendRequest(Long requestId) { throw new BusinessException(ErrorCode.FRIEND_REQUEST_ALREADY_ACCEPTED_OR_REJECTED); } - friendRequest.setStatus(FriendRequest.Status.REJECTED); - friendRequestRepository.save(friendRequest); + if (friendRequest.getRequester().getId().equals(userId)) { + throw new BusinessException(ErrorCode.FRIEND_REQUEST_NOT_RECEIVER); + } + + FriendRequest updatedFriendRequest = FriendRequest.builder() + .id(friendRequest.getId()) + .requester(friendRequest.getRequester()) + .receiver(friendRequest.getReceiver()) + .status(FriendRequest.Status.REJECTED) + .createdAt(friendRequest.getCreatedAt()) + .updatedAt(LocalDateTime.now()) + .build(); + + friendRequestRepository.save(updatedFriendRequest); User requester = friendRequest.getRequester(); @@ -108,7 +140,7 @@ public ReceivedFriendRequestResponse rejectFriendRequest(Long requestId) { friendRequest.getId(), requester.getId(), requester.getNickname(), - friendRequest.getStatus().name(), + updatedFriendRequest.getStatus().name(), requester.getProfileImage() ); } From 0580d03bd71178b0d7a9ed332f525bcddd6d3f9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=84=B8=EC=A7=84?= Date: Mon, 14 Oct 2024 01:00:12 +0900 Subject: [PATCH 11/46] =?UTF-8?q?test:=20=EB=A6=AC=EB=B7=B0=20=EB=B0=98?= =?UTF-8?q?=EC=98=81=ED=95=98=EC=97=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../friend/service/FriendServiceTest.java | 34 +++++++++++---- .../service/FriendRequestServiceTest.java | 42 +++++++++---------- 2 files changed, 47 insertions(+), 29 deletions(-) diff --git a/src/test/java/com/splanet/splanet/friend/service/FriendServiceTest.java b/src/test/java/com/splanet/splanet/friend/service/FriendServiceTest.java index 171e1bed..65f488a4 100644 --- a/src/test/java/com/splanet/splanet/friend/service/FriendServiceTest.java +++ b/src/test/java/com/splanet/splanet/friend/service/FriendServiceTest.java @@ -49,7 +49,7 @@ void setUp() { } @Test - void getFriends_성공() { + void 친구목록조회_성공() { // Arrange when(mockFriend.getFriend()).thenReturn(mockUser); when(mockUser.getNickname()).thenReturn("testUser"); @@ -67,16 +67,19 @@ void setUp() { } @Test - void getFriendPlan_성공() { + void 친구플랜조회_성공() { // Arrange LocalDateTime startDate = LocalDateTime.of(2024, 1, 1, 0, 0); LocalDateTime endDate = LocalDateTime.of(2024, 12, 31, 0, 0); + // 친구 확인 모킹 + when(friendRepository.existsByUserIdAndFriendId(1L, 1L)).thenReturn(true); + when(mockPlan.getId()).thenReturn(1L); when(mockPlan.getTitle()).thenReturn("Test Plan"); when(mockPlan.getDescription()).thenReturn("This is a test plan."); - when(mockPlan.getStartDate()).thenReturn(startDate); // LocalDateTime 반환 - when(mockPlan.getEndDate()).thenReturn(endDate); // LocalDateTime 반환 + when(mockPlan.getStartDate()).thenReturn(startDate); + when(mockPlan.getEndDate()).thenReturn(endDate); when(mockPlan.getAccessibility()).thenReturn(true); when(mockPlan.getIsCompleted()).thenReturn(false); when(mockPlan.getCreatedAt()).thenReturn(null); @@ -94,14 +97,29 @@ void setUp() { } @Test - void getFriendPlan_플랜없음_예외발생() { + void 친구플랜조회_성공_플랜없음() { + // Arrange + when(friendRepository.existsByUserIdAndFriendId(1L, 1L)).thenReturn(true); // 친구 확인 성공 + when(planRepository.findAllByUserIdAndAccessibility(1L, true)).thenReturn(Collections.emptyList()); // 플랜 없음 + + // Act + ResponseEntity> response = friendService.getFriendPlan(1L, 1L); + + // Assert + assertNotNull(response); + assertEquals(200, response.getStatusCodeValue()); // 성공적으로 빈 목록을 반환해야 함 + assertTrue(response.getBody().isEmpty()); // 빈 목록인지 확인 + } + + @Test + void 친구플랜조회_실패_친구아님() { // Arrange - when(planRepository.findAllByUserIdAndAccessibility(1L, true)).thenReturn(Collections.emptyList()); + when(friendRepository.existsByUserIdAndFriendId(1L, 2L)).thenReturn(false); // 친구가 아닌 경우 // Act & Assert BusinessException exception = assertThrows(BusinessException.class, () -> { - friendService.getFriendPlan(1L, 1L); + friendService.getFriendPlan(2L, 1L); // 다른 userId로 조회 }); - assertEquals(ErrorCode.PLAN_NOT_FOUND, exception.getErrorCode()); + assertEquals(ErrorCode.FRIEND_NOT_FOUND, exception.getErrorCode()); } } \ No newline at end of file diff --git a/src/test/java/com/splanet/splanet/friendRequest/service/FriendRequestServiceTest.java b/src/test/java/com/splanet/splanet/friendRequest/service/FriendRequestServiceTest.java index e24517ef..dc0cc806 100644 --- a/src/test/java/com/splanet/splanet/friendRequest/service/FriendRequestServiceTest.java +++ b/src/test/java/com/splanet/splanet/friendRequest/service/FriendRequestServiceTest.java @@ -12,12 +12,12 @@ import com.splanet.splanet.user.repository.UserRepository; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import java.util.Arrays; -import java.util.Collections; import java.util.List; import java.util.Optional; @@ -47,7 +47,6 @@ class FriendRequestServiceTest { void setUp() { MockitoAnnotations.openMocks(this); - // 유저 객체 생성 requester = User.builder() .id(1L) .nickname("요청자") @@ -60,7 +59,6 @@ void setUp() { .profileImage("receiver.png") .build(); - // 친구 요청 객체 생성 friendRequest = FriendRequest.builder() .id(1L) .requester(requester) @@ -86,10 +84,9 @@ void setUp() { @Test void 친구요청전송_본인에게요청() { Long userId = requester.getId(); - Long receiverId = requester.getId(); BusinessException exception = assertThrows(BusinessException.class, () -> - friendRequestService.sendFriendRequest(userId, receiverId) + friendRequestService.sendFriendRequest(userId, userId) ); assertEquals(ErrorCode.SELF_FRIEND_REQUEST_NOT_ALLOWED, exception.getErrorCode()); @@ -101,13 +98,15 @@ void setUp() { when(friendRequestRepository.findById(requestId)).thenReturn(Optional.of(friendRequest)); - // 직접 상태를 설정 - friendRequest.setStatus(FriendRequest.Status.PENDING); + ReceivedFriendRequestResponse response = friendRequestService.acceptFriendRequest(requestId, receiver.getId()); + + assertEquals(requester.getId(), response.requesterId()); - ReceivedFriendRequestResponse response = friendRequestService.acceptFriendRequest(requestId); + ArgumentCaptor friendRequestCaptor = ArgumentCaptor.forClass(FriendRequest.class); + verify(friendRequestRepository, times(1)).save(friendRequestCaptor.capture()); - assertEquals(requester.getId(), response.requesterId()); // 요청자의 ID가 맞는지 확인 - verify(friendRequestRepository, times(1)).save(friendRequest); + FriendRequest savedFriendRequest = friendRequestCaptor.getValue(); + assertEquals(FriendRequest.Status.ACCEPTED, savedFriendRequest.getStatus()); verify(friendRepository, times(2)).save(any(Friend.class)); } @@ -118,7 +117,7 @@ void setUp() { when(friendRequestRepository.findById(requestId)).thenReturn(Optional.empty()); BusinessException exception = assertThrows(BusinessException.class, () -> - friendRequestService.acceptFriendRequest(requestId) + friendRequestService.acceptFriendRequest(requestId, receiver.getId()) ); assertEquals(ErrorCode.FRIEND_REQUEST_NOT_FOUND, exception.getErrorCode()); @@ -130,13 +129,14 @@ void setUp() { when(friendRequestRepository.findById(requestId)).thenReturn(Optional.of(friendRequest)); - // 직접 상태를 설정 - friendRequest.setStatus(FriendRequest.Status.PENDING); + ReceivedFriendRequestResponse response = friendRequestService.rejectFriendRequest(requestId, receiver.getId()); - ReceivedFriendRequestResponse response = friendRequestService.rejectFriendRequest(requestId); + assertEquals(requester.getId(), response.requesterId()); - assertEquals(requester.getId(), response.requesterId()); // 요청자의 ID가 맞는지 확인 - verify(friendRequestRepository, times(1)).save(friendRequest); + verify(friendRequestRepository, times(1)).save(argThat(savedFriendRequest -> + savedFriendRequest.getId().equals(friendRequest.getId()) && + savedFriendRequest.getStatus() == FriendRequest.Status.REJECTED + )); } @Test @@ -166,7 +166,7 @@ void setUp() { @Test void 친구요청전송_수신자존재하지않음() { Long userId = requester.getId(); - Long receiverId = 999L; // 존재하지 않는 ID + Long receiverId = 999L; when(userRepository.findById(receiverId)).thenReturn(Optional.empty()); @@ -180,12 +180,12 @@ void setUp() { @Test void 친구요청수락_이미수락된요청() { Long requestId = friendRequest.getId(); - friendRequest.setStatus(FriendRequest.Status.ACCEPTED); // 상태를 수락으로 설정 + friendRequest.setStatus(FriendRequest.Status.ACCEPTED); when(friendRequestRepository.findById(requestId)).thenReturn(Optional.of(friendRequest)); BusinessException exception = assertThrows(BusinessException.class, () -> - friendRequestService.acceptFriendRequest(requestId) + friendRequestService.acceptFriendRequest(requestId, receiver.getId()) ); assertEquals(ErrorCode.FRIEND_REQUEST_ALREADY_ACCEPTED_OR_REJECTED, exception.getErrorCode()); @@ -194,12 +194,12 @@ void setUp() { @Test void 친구요청수락_이미거절된요청() { Long requestId = friendRequest.getId(); - friendRequest.setStatus(FriendRequest.Status.REJECTED); // 상태를 거절로 설정 + friendRequest.setStatus(FriendRequest.Status.REJECTED); when(friendRequestRepository.findById(requestId)).thenReturn(Optional.of(friendRequest)); BusinessException exception = assertThrows(BusinessException.class, () -> - friendRequestService.acceptFriendRequest(requestId) + friendRequestService.acceptFriendRequest(requestId, receiver.getId()) ); assertEquals(ErrorCode.FRIEND_REQUEST_ALREADY_ACCEPTED_OR_REJECTED, exception.getErrorCode()); From 58d89611bac074f075070d925e4ff350342efc46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=84=B8=EC=A7=84?= Date: Mon, 14 Oct 2024 01:09:30 +0900 Subject: [PATCH 12/46] =?UTF-8?q?test:=20=ED=95=84=EC=9A=94=ED=95=9C=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=BD=94=EB=93=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../friend/service/FriendServiceTest.java | 47 ++++++++++++++ .../service/FriendRequestServiceTest.java | 63 +++++++++++++++++++ 2 files changed, 110 insertions(+) diff --git a/src/test/java/com/splanet/splanet/friend/service/FriendServiceTest.java b/src/test/java/com/splanet/splanet/friend/service/FriendServiceTest.java index 65f488a4..e5b0f11f 100644 --- a/src/test/java/com/splanet/splanet/friend/service/FriendServiceTest.java +++ b/src/test/java/com/splanet/splanet/friend/service/FriendServiceTest.java @@ -111,6 +111,28 @@ void setUp() { assertTrue(response.getBody().isEmpty()); // 빈 목록인지 확인 } + @Test + void 친구플랜조회_성공_여러플랜() { + // Arrange + Plan mockPlan1 = mock(Plan.class); + Plan mockPlan2 = mock(Plan.class); + + when(friendRepository.existsByUserIdAndFriendId(1L, 1L)).thenReturn(true); + when(mockPlan1.getTitle()).thenReturn("Plan 1"); + when(mockPlan2.getTitle()).thenReturn("Plan 2"); + when(planRepository.findAllByUserIdAndAccessibility(1L, true)).thenReturn(List.of(mockPlan1, mockPlan2)); + + // Act + ResponseEntity> response = friendService.getFriendPlan(1L, 1L); + + // Assert + assertNotNull(response); + assertEquals(200, response.getStatusCodeValue()); + assertEquals(2, response.getBody().size()); + assertEquals("Plan 1", response.getBody().get(0).getTitle()); + assertEquals("Plan 2", response.getBody().get(1).getTitle()); + } + @Test void 친구플랜조회_실패_친구아님() { // Arrange @@ -122,4 +144,29 @@ void setUp() { }); assertEquals(ErrorCode.FRIEND_NOT_FOUND, exception.getErrorCode()); } + + @Test + void 친구목록조회_실패_사용자없음() { + // Arrange + when(friendRepository.findByUserId(99L)).thenReturn(Collections.emptyList()); // 존재하지 않는 사용자 + + // Act + List friends = friendService.getFriends(99L); + + // Assert + assertNotNull(friends); + assertTrue(friends.isEmpty()); // 빈 목록이 반환되어야 함 + } + + @Test + void 친구플랜조회_실패_친구관계없음() { + // Arrange + when(friendRepository.existsByUserIdAndFriendId(1L, 2L)).thenReturn(false); // 친구 관계가 없음 + + // Act & Assert + BusinessException exception = assertThrows(BusinessException.class, () -> { + friendService.getFriendPlan(1L, 2L); + }); + assertEquals(ErrorCode.FRIEND_NOT_FOUND, exception.getErrorCode()); + } } \ No newline at end of file diff --git a/src/test/java/com/splanet/splanet/friendRequest/service/FriendRequestServiceTest.java b/src/test/java/com/splanet/splanet/friendRequest/service/FriendRequestServiceTest.java index dc0cc806..4f0b864d 100644 --- a/src/test/java/com/splanet/splanet/friendRequest/service/FriendRequestServiceTest.java +++ b/src/test/java/com/splanet/splanet/friendRequest/service/FriendRequestServiceTest.java @@ -18,6 +18,7 @@ import org.mockito.MockitoAnnotations; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.Optional; @@ -139,6 +140,30 @@ void setUp() { )); } + @Test + void 친구요청수락_요청자와수락자가동일() { + Long requestId = friendRequest.getId(); + when(friendRequestRepository.findById(requestId)).thenReturn(Optional.of(friendRequest)); + + BusinessException exception = assertThrows(BusinessException.class, () -> + friendRequestService.acceptFriendRequest(requestId, requester.getId()) + ); + + assertEquals(ErrorCode.FRIEND_REQUEST_NOT_RECEIVER, exception.getErrorCode()); + } + + @Test + void 친구요청거절_요청자와수락자가동일() { + Long requestId = friendRequest.getId(); + when(friendRequestRepository.findById(requestId)).thenReturn(Optional.of(friendRequest)); + + BusinessException exception = assertThrows(BusinessException.class, () -> + friendRequestService.rejectFriendRequest(requestId, requester.getId()) + ); + + assertEquals(ErrorCode.FRIEND_REQUEST_NOT_RECEIVER, exception.getErrorCode()); + } + @Test void 친구요청목록조회_받은요청() { Long userId = receiver.getId(); @@ -163,6 +188,28 @@ void setUp() { assertEquals(receiver.getId(), responses.get(0).receiverId()); } + @Test + void 친구요청목록조회_받은요청없음() { + Long userId = receiver.getId(); + + when(friendRequestRepository.findByReceiverId(userId)).thenReturn(Collections.emptyList()); + + List responses = friendRequestService.getReceivedFriendRequests(userId); + + assertEquals(0, responses.size()); // 빈 목록인지 확인 + } + + @Test + void 친구요청목록조회_보낸요청없음() { + Long userId = requester.getId(); + + when(friendRequestRepository.findByRequesterId(userId)).thenReturn(Collections.emptyList()); + + List responses = friendRequestService.getSentFriendRequests(userId); + + assertEquals(0, responses.size()); // 빈 목록인지 확인 + } + @Test void 친구요청전송_수신자존재하지않음() { Long userId = requester.getId(); @@ -177,6 +224,22 @@ void setUp() { assertEquals(ErrorCode.USER_NOT_FOUND, exception.getErrorCode()); } + @Test + void 친구요청전송_이미친구인경우() { + Long userId = requester.getId(); + Long receiverId = receiver.getId(); + + when(userRepository.findById(receiverId)).thenReturn(Optional.of(receiver)); + when(userRepository.findById(userId)).thenReturn(Optional.of(requester)); + when(friendRepository.existsByUserIdAndFriendId(userId, receiverId)).thenReturn(true); // 이미 친구인 경우 + + BusinessException exception = assertThrows(BusinessException.class, () -> + friendRequestService.sendFriendRequest(userId, receiverId) + ); + + assertEquals(ErrorCode.FRIEND_ALREADY_EXISTS, exception.getErrorCode()); + } + @Test void 친구요청수락_이미수락된요청() { Long requestId = friendRequest.getId(); From 3c116e705e6a1ec012a2cacb58b0671f3d29ae94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=84=B8=EC=A7=84?= Date: Mon, 14 Oct 2024 01:16:42 +0900 Subject: [PATCH 13/46] =?UTF-8?q?chore:=20=EC=B9=9C=EA=B5=AC=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20=EC=95=88=EB=8F=BC=EC=84=9C=20=EB=8B=A4=EC=8B=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=ED=95=98=EA=B2=A0=EC=8A=B5=EB=8B=88=EB=8B=A4?= =?UTF-8?q?!?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/splanet/splanet/friend/service/FriendService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/splanet/splanet/friend/service/FriendService.java b/src/main/java/com/splanet/splanet/friend/service/FriendService.java index 283da25a..3e263a92 100644 --- a/src/main/java/com/splanet/splanet/friend/service/FriendService.java +++ b/src/main/java/com/splanet/splanet/friend/service/FriendService.java @@ -83,7 +83,7 @@ public ResponseEntity> getFriendPlan(Long friendId, Long u return ResponseEntity.ok(planResponseDtos); } - // 친구 삭제(취소)하기 + // 친구 삭제(취소)하기 -> 500 떠서 고치고 다시 푸시할게요! public ResponseEntity> unfriend(Long friendId, Long userId) { if (!friendRepository.existsByUserIdAndFriendId(userId, friendId)) { throw new BusinessException(ErrorCode.FRIEND_NOT_FOUND); From 4451c9cdc8ecab4e48292114b7535d7acbe0715d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=84=B8=EC=A7=84?= Date: Mon, 14 Oct 2024 01:20:41 +0900 Subject: [PATCH 14/46] =?UTF-8?q?refactor:=20=EB=88=84=EB=9D=BD=EB=90=9C?= =?UTF-8?q?=20=ED=8A=B8=EB=9E=99=EC=9E=AC=EC=85=98=20=EC=96=B4=EB=85=B8?= =?UTF-8?q?=ED=85=8C=EC=9D=B4=EC=85=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/splanet/splanet/friend/service/FriendService.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/splanet/splanet/friend/service/FriendService.java b/src/main/java/com/splanet/splanet/friend/service/FriendService.java index 3e263a92..93d6891d 100644 --- a/src/main/java/com/splanet/splanet/friend/service/FriendService.java +++ b/src/main/java/com/splanet/splanet/friend/service/FriendService.java @@ -12,6 +12,7 @@ import com.splanet.splanet.plan.repository.PlanRepository; import com.splanet.splanet.user.entity.User; import com.splanet.splanet.user.repository.UserRepository; +import jakarta.transaction.Transactional; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; @@ -83,7 +84,8 @@ public ResponseEntity> getFriendPlan(Long friendId, Long u return ResponseEntity.ok(planResponseDtos); } - // 친구 삭제(취소)하기 -> 500 떠서 고치고 다시 푸시할게요! + // 친구 삭제(취소)하기 + @Transactional public ResponseEntity> unfriend(Long friendId, Long userId) { if (!friendRepository.existsByUserIdAndFriendId(userId, friendId)) { throw new BusinessException(ErrorCode.FRIEND_NOT_FOUND); From 0813abd349ea07031530b791940f3eb379d9f100 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=84=B8=EC=A7=84?= Date: Mon, 14 Oct 2024 23:23:18 +0900 Subject: [PATCH 15/46] =?UTF-8?q?refactor:=20=EC=9A=94=EC=B2=AD=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=EC=A1=B0=ED=9A=8C=20=EC=98=88=EC=99=B8=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../splanet/core/exception/ErrorCode.java | 3 +- .../repository/FriendRequestRepository.java | 8 +++++ .../service/FriendRequestService.java | 29 +++++++++++++++++-- 3 files changed, 36 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/splanet/splanet/core/exception/ErrorCode.java b/src/main/java/com/splanet/splanet/core/exception/ErrorCode.java index 9431b1e5..8d641d20 100644 --- a/src/main/java/com/splanet/splanet/core/exception/ErrorCode.java +++ b/src/main/java/com/splanet/splanet/core/exception/ErrorCode.java @@ -39,9 +39,10 @@ public enum ErrorCode { // friend FRIEND_NOT_FOUND("친구가 아닙니다.",HttpStatus.NOT_FOUND), - FRIEND_REQUEST_NOT_FOUND("받은 친구 요청이 없습니다.", HttpStatus.NOT_FOUND), + FRIEND_REQUEST_NOT_FOUND("받은 친구 요청이 아무것도 없습니다.", HttpStatus.NOT_FOUND), FRIEND_ALREADY_EXISTS("이미 친구 목록에 있습니다.", HttpStatus.BAD_REQUEST), FRIEND_REQUEST_ALREADY_SENT("이미 요청을 보냈습니다.",HttpStatus.BAD_REQUEST), + FRIEND_REQUEST_NOT_FOUND_IN_RECEIVED_LIST("내가 받은 요청이 아닙니다.", HttpStatus.NOT_FOUND), FRIEND_REQUEST_NOT_RECEIVER("본인이 보낸 요청은 수락하거나 거절할 수 없습니다.", HttpStatus.BAD_REQUEST), FRIEND_REQUEST_ALREADY_ACCEPTED_OR_REJECTED("이미 수락하거나 거절한 사용자 입니다.", HttpStatus.BAD_REQUEST), SELF_FRIEND_REQUEST_NOT_ALLOWED("본인에게 친구요청을 보낼 수 없습니다.", HttpStatus.BAD_REQUEST); diff --git a/src/main/java/com/splanet/splanet/friendRequest/repository/FriendRequestRepository.java b/src/main/java/com/splanet/splanet/friendRequest/repository/FriendRequestRepository.java index 196405c8..d9dc35b2 100644 --- a/src/main/java/com/splanet/splanet/friendRequest/repository/FriendRequestRepository.java +++ b/src/main/java/com/splanet/splanet/friendRequest/repository/FriendRequestRepository.java @@ -10,6 +10,13 @@ @Repository public interface FriendRequestRepository extends JpaRepository { + + @Query("SELECT fr FROM FriendRequest fr JOIN FETCH fr.requester WHERE fr.receiver.id = :userId") + List findByReceiverIdWithRequester(@Param("userId") Long userId); + + @Query("SELECT fr FROM FriendRequest fr JOIN FETCH fr.receiver WHERE fr.requester.id = :userId") + List findByRequesterIdWithReceiver(@Param("userId") Long userId); + @Query("SELECT fr FROM FriendRequest fr WHERE fr.receiver.id = :userId") List findByReceiverId(@Param("userId") Long userId); @@ -18,4 +25,5 @@ public interface FriendRequestRepository extends JpaRepository findPendingRequestsByReceiverId(@Param("receiverId") Long receiverId, @Param("requesterId") Long requesterId, @Param("status") FriendRequest.Status status); + } \ No newline at end of file diff --git a/src/main/java/com/splanet/splanet/friendRequest/service/FriendRequestService.java b/src/main/java/com/splanet/splanet/friendRequest/service/FriendRequestService.java index d9c5ff80..e0420592 100644 --- a/src/main/java/com/splanet/splanet/friendRequest/service/FriendRequestService.java +++ b/src/main/java/com/splanet/splanet/friendRequest/service/FriendRequestService.java @@ -74,6 +74,21 @@ public ReceivedFriendRequestResponse acceptFriendRequest(Long requestId, Long us throw new BusinessException(ErrorCode.FRIEND_REQUEST_NOT_RECEIVER); } + List receivedRequests = getReceivedFriendRequests(userId); + boolean isRequestPresent = receivedRequests.stream() + .anyMatch(request -> request.requesterId().equals(friendRequest.getRequester().getId())); + + /** 내 요청목록에 없는 요청id를 조회하면 FRIEND_REQUEST_NOT_FOUND_IN_RECEIVED_LIST가 안뜨고 + FRIEND_REQUEST_NOT_RECEIVER가 뜸.. */ + if (!isRequestPresent) { + // 요청자가 보낸 요청인 경우에만 + if (friendRequest.getRequester().getId().equals(userId)) { + throw new BusinessException(ErrorCode.FRIEND_REQUEST_NOT_FOUND_IN_RECEIVED_LIST); + } + // 요청이 보낸 요청이 아닌 경우에 대해 + throw new BusinessException(ErrorCode.FRIEND_REQUEST_NOT_RECEIVER); + } + FriendRequest updatedFriendRequest = FriendRequest.builder() .id(friendRequest.getId()) .requester(friendRequest.getRequester()) @@ -119,10 +134,18 @@ public ReceivedFriendRequestResponse rejectFriendRequest(Long requestId, Long us throw new BusinessException(ErrorCode.FRIEND_REQUEST_ALREADY_ACCEPTED_OR_REJECTED); } - if (friendRequest.getRequester().getId().equals(userId)) { + if (!friendRequest.getReceiver().getId().equals(userId)) { throw new BusinessException(ErrorCode.FRIEND_REQUEST_NOT_RECEIVER); } + List receivedRequests = getReceivedFriendRequests(userId); + boolean isRequestPresent = receivedRequests.stream() + .anyMatch(request -> request.requesterId().equals(friendRequest.getRequester().getId())); + + if (!isRequestPresent) { + throw new BusinessException(ErrorCode.FRIEND_REQUEST_NOT_FOUND_IN_RECEIVED_LIST); + } + FriendRequest updatedFriendRequest = FriendRequest.builder() .id(friendRequest.getId()) .requester(friendRequest.getRequester()) @@ -147,7 +170,7 @@ public ReceivedFriendRequestResponse rejectFriendRequest(Long requestId, Long us // 친구 요청 목록 조회(받은 요청) public List getReceivedFriendRequests(Long userId) { - List requests = friendRequestRepository.findByReceiverId(userId); + List requests = friendRequestRepository.findByReceiverIdWithRequester(userId); // PENDING인 요청만 return requests.stream() @@ -164,7 +187,7 @@ public List getReceivedFriendRequests(Long userId // 친구 요청 목록 조회(보낸 요청) public List getSentFriendRequests(Long userId) { - List requests = friendRequestRepository.findByRequesterId(userId); + List requests = friendRequestRepository.findByRequesterIdWithReceiver(userId); // PENDING인 요청만 return requests.stream() From b8d29f8ce318e2c5ec5d14e6850ebcec9a2cc48d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=84=B8=EC=A7=84?= Date: Tue, 15 Oct 2024 00:43:50 +0900 Subject: [PATCH 16/46] =?UTF-8?q?refactor:=20=EC=98=88=EC=99=B8=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=20=EC=88=98=EC=A0=95=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../splanet/core/exception/ErrorCode.java | 2 +- .../repository/FriendRequestRepository.java | 7 ++--- .../service/FriendRequestService.java | 29 ++++++++++++------- 3 files changed, 23 insertions(+), 15 deletions(-) diff --git a/src/main/java/com/splanet/splanet/core/exception/ErrorCode.java b/src/main/java/com/splanet/splanet/core/exception/ErrorCode.java index 8d641d20..88ff90d7 100644 --- a/src/main/java/com/splanet/splanet/core/exception/ErrorCode.java +++ b/src/main/java/com/splanet/splanet/core/exception/ErrorCode.java @@ -39,7 +39,7 @@ public enum ErrorCode { // friend FRIEND_NOT_FOUND("친구가 아닙니다.",HttpStatus.NOT_FOUND), - FRIEND_REQUEST_NOT_FOUND("받은 친구 요청이 아무것도 없습니다.", HttpStatus.NOT_FOUND), + FRIEND_REQUEST_NOT_FOUND("해당 친구 요청을 찾을 수 없습니다.", HttpStatus.NOT_FOUND), FRIEND_ALREADY_EXISTS("이미 친구 목록에 있습니다.", HttpStatus.BAD_REQUEST), FRIEND_REQUEST_ALREADY_SENT("이미 요청을 보냈습니다.",HttpStatus.BAD_REQUEST), FRIEND_REQUEST_NOT_FOUND_IN_RECEIVED_LIST("내가 받은 요청이 아닙니다.", HttpStatus.NOT_FOUND), diff --git a/src/main/java/com/splanet/splanet/friendRequest/repository/FriendRequestRepository.java b/src/main/java/com/splanet/splanet/friendRequest/repository/FriendRequestRepository.java index d9dc35b2..96ff90e3 100644 --- a/src/main/java/com/splanet/splanet/friendRequest/repository/FriendRequestRepository.java +++ b/src/main/java/com/splanet/splanet/friendRequest/repository/FriendRequestRepository.java @@ -17,13 +17,12 @@ public interface FriendRequestRepository extends JpaRepository findByRequesterIdWithReceiver(@Param("userId") Long userId); + @Query("SELECT fr FROM FriendRequest fr WHERE fr.receiver.id = :receiverId AND fr.requester.id = :requesterId AND fr.status = :status") + List findPendingRequestsByReceiverId(@Param("receiverId") Long receiverId, @Param("requesterId") Long requesterId, @Param("status") FriendRequest.Status status); + @Query("SELECT fr FROM FriendRequest fr WHERE fr.receiver.id = :userId") List findByReceiverId(@Param("userId") Long userId); @Query("SELECT fr FROM FriendRequest fr WHERE fr.requester.id = :userId") List findByRequesterId(@Param("userId") Long userId); - - @Query("SELECT fr FROM FriendRequest fr WHERE fr.receiver.id = :receiverId AND fr.requester.id = :requesterId AND fr.status = :status") - List findPendingRequestsByReceiverId(@Param("receiverId") Long receiverId, @Param("requesterId") Long requesterId, @Param("status") FriendRequest.Status status); - } \ No newline at end of file diff --git a/src/main/java/com/splanet/splanet/friendRequest/service/FriendRequestService.java b/src/main/java/com/splanet/splanet/friendRequest/service/FriendRequestService.java index e0420592..19f27554 100644 --- a/src/main/java/com/splanet/splanet/friendRequest/service/FriendRequestService.java +++ b/src/main/java/com/splanet/splanet/friendRequest/service/FriendRequestService.java @@ -70,23 +70,24 @@ public ReceivedFriendRequestResponse acceptFriendRequest(Long requestId, Long us throw new BusinessException(ErrorCode.FRIEND_REQUEST_ALREADY_ACCEPTED_OR_REJECTED); } - if (!friendRequest.getReceiver().getId().equals(userId)) { + // 요청자 본인이 수락을 시도할 경우, 내가 보낸 요청 처리 할 수 없음 + if (friendRequest.getRequester().getId().equals(userId)) { throw new BusinessException(ErrorCode.FRIEND_REQUEST_NOT_RECEIVER); } + // 내가 받은 요청이 아닐 경우, 예외처리 + if (!friendRequest.getReceiver().getId().equals(userId)) { + throw new BusinessException(ErrorCode.FRIEND_REQUEST_NOT_FOUND_IN_RECEIVED_LIST); + } + List receivedRequests = getReceivedFriendRequests(userId); + boolean isRequestPresent = receivedRequests.stream() .anyMatch(request -> request.requesterId().equals(friendRequest.getRequester().getId())); - /** 내 요청목록에 없는 요청id를 조회하면 FRIEND_REQUEST_NOT_FOUND_IN_RECEIVED_LIST가 안뜨고 - FRIEND_REQUEST_NOT_RECEIVER가 뜸.. */ + // 받은 요청 목록에 없어도 예외처리 if (!isRequestPresent) { - // 요청자가 보낸 요청인 경우에만 - if (friendRequest.getRequester().getId().equals(userId)) { - throw new BusinessException(ErrorCode.FRIEND_REQUEST_NOT_FOUND_IN_RECEIVED_LIST); - } - // 요청이 보낸 요청이 아닌 경우에 대해 - throw new BusinessException(ErrorCode.FRIEND_REQUEST_NOT_RECEIVER); + throw new BusinessException(ErrorCode.FRIEND_REQUEST_NOT_FOUND_IN_RECEIVED_LIST); } FriendRequest updatedFriendRequest = FriendRequest.builder() @@ -134,14 +135,22 @@ public ReceivedFriendRequestResponse rejectFriendRequest(Long requestId, Long us throw new BusinessException(ErrorCode.FRIEND_REQUEST_ALREADY_ACCEPTED_OR_REJECTED); } - if (!friendRequest.getReceiver().getId().equals(userId)) { + // 요청자 본인이 거절을 시도할 경우, 내가 보낸 요청 처리 할 수 없음 + if (friendRequest.getRequester().getId().equals(userId)) { throw new BusinessException(ErrorCode.FRIEND_REQUEST_NOT_RECEIVER); } + // 내가 받은 요청이 아닐 경우, 예외처리 + if (!friendRequest.getReceiver().getId().equals(userId)) { + throw new BusinessException(ErrorCode.FRIEND_REQUEST_NOT_FOUND_IN_RECEIVED_LIST); + } + List receivedRequests = getReceivedFriendRequests(userId); + boolean isRequestPresent = receivedRequests.stream() .anyMatch(request -> request.requesterId().equals(friendRequest.getRequester().getId())); + // 받은 요청 목록에 없어도 예외처리 if (!isRequestPresent) { throw new BusinessException(ErrorCode.FRIEND_REQUEST_NOT_FOUND_IN_RECEIVED_LIST); } From dd80ba112f82e3e4e791fae1cecc2dbc3ba834cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=84=B8=EC=A7=84?= Date: Tue, 15 Oct 2024 01:18:02 +0900 Subject: [PATCH 17/46] =?UTF-8?q?test:=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/FriendRequestServiceTest.java | 42 ++----------------- 1 file changed, 3 insertions(+), 39 deletions(-) diff --git a/src/test/java/com/splanet/splanet/friendRequest/service/FriendRequestServiceTest.java b/src/test/java/com/splanet/splanet/friendRequest/service/FriendRequestServiceTest.java index 4f0b864d..e07e84f8 100644 --- a/src/test/java/com/splanet/splanet/friendRequest/service/FriendRequestServiceTest.java +++ b/src/test/java/com/splanet/splanet/friendRequest/service/FriendRequestServiceTest.java @@ -2,7 +2,6 @@ import com.splanet.splanet.core.exception.BusinessException; import com.splanet.splanet.core.exception.ErrorCode; -import com.splanet.splanet.friend.entity.Friend; import com.splanet.splanet.friend.repository.FriendRepository; import com.splanet.splanet.friendRequest.dto.ReceivedFriendRequestResponse; import com.splanet.splanet.friendRequest.dto.SentFriendRequestResponse; @@ -12,7 +11,6 @@ import com.splanet.splanet.user.repository.UserRepository; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockitoAnnotations; @@ -93,24 +91,6 @@ void setUp() { assertEquals(ErrorCode.SELF_FRIEND_REQUEST_NOT_ALLOWED, exception.getErrorCode()); } - @Test - void 친구요청수락_성공() { - Long requestId = friendRequest.getId(); - - when(friendRequestRepository.findById(requestId)).thenReturn(Optional.of(friendRequest)); - - ReceivedFriendRequestResponse response = friendRequestService.acceptFriendRequest(requestId, receiver.getId()); - - assertEquals(requester.getId(), response.requesterId()); - - ArgumentCaptor friendRequestCaptor = ArgumentCaptor.forClass(FriendRequest.class); - verify(friendRequestRepository, times(1)).save(friendRequestCaptor.capture()); - - FriendRequest savedFriendRequest = friendRequestCaptor.getValue(); - assertEquals(FriendRequest.Status.ACCEPTED, savedFriendRequest.getStatus()); - verify(friendRepository, times(2)).save(any(Friend.class)); - } - @Test void 친구요청수락_요청없음() { Long requestId = 1L; @@ -124,22 +104,6 @@ void setUp() { assertEquals(ErrorCode.FRIEND_REQUEST_NOT_FOUND, exception.getErrorCode()); } - @Test - void 친구요청거절_성공() { - Long requestId = friendRequest.getId(); - - when(friendRequestRepository.findById(requestId)).thenReturn(Optional.of(friendRequest)); - - ReceivedFriendRequestResponse response = friendRequestService.rejectFriendRequest(requestId, receiver.getId()); - - assertEquals(requester.getId(), response.requesterId()); - - verify(friendRequestRepository, times(1)).save(argThat(savedFriendRequest -> - savedFriendRequest.getId().equals(friendRequest.getId()) && - savedFriendRequest.getStatus() == FriendRequest.Status.REJECTED - )); - } - @Test void 친구요청수락_요청자와수락자가동일() { Long requestId = friendRequest.getId(); @@ -168,7 +132,7 @@ void setUp() { void 친구요청목록조회_받은요청() { Long userId = receiver.getId(); - when(friendRequestRepository.findByReceiverId(userId)).thenReturn(Arrays.asList(friendRequest)); + when(friendRequestRepository.findByReceiverIdWithRequester(userId)).thenReturn(Arrays.asList(friendRequest)); List responses = friendRequestService.getReceivedFriendRequests(userId); @@ -180,7 +144,7 @@ void setUp() { void 친구요청목록조회_보낸요청() { Long userId = requester.getId(); - when(friendRequestRepository.findByRequesterId(userId)).thenReturn(Arrays.asList(friendRequest)); + when(friendRequestRepository.findByRequesterIdWithReceiver(userId)).thenReturn(Arrays.asList(friendRequest)); List responses = friendRequestService.getSentFriendRequests(userId); @@ -192,7 +156,7 @@ void setUp() { void 친구요청목록조회_받은요청없음() { Long userId = receiver.getId(); - when(friendRequestRepository.findByReceiverId(userId)).thenReturn(Collections.emptyList()); + when(friendRequestRepository.findByReceiverIdWithRequester(userId)).thenReturn(Collections.emptyList()); List responses = friendRequestService.getReceivedFriendRequests(userId); From 25a9870f713a559ab2e3d404ec5d9a7dcf7e5789 Mon Sep 17 00:00:00 2001 From: kanguk Date: Tue, 15 Oct 2024 02:35:32 +0900 Subject: [PATCH 18/46] =?UTF-8?q?feat:=20previewPlan=20TTL=EC=9D=84=20?= =?UTF-8?q?=EC=A0=9C=EB=8C=80=EB=A1=9C=20=EC=A0=81=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EC=9D=80=20=EB=AC=B8=EC=A0=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TTL이 제대로 설정되어있지 않던 문제를 수정한다. --- .../com/splanet/splanet/previewplan/entity/PlanCard.java | 4 ++-- .../com/splanet/splanet/previewplan/entity/PlanGroup.java | 4 ++-- .../com/splanet/splanet/previewplan/entity/PreviewPlan.java | 6 ++++-- .../splanet/previewplan/service/PreviewPlanService.java | 4 ++++ 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/splanet/splanet/previewplan/entity/PlanCard.java b/src/main/java/com/splanet/splanet/previewplan/entity/PlanCard.java index 43f3f28d..54121a5f 100644 --- a/src/main/java/com/splanet/splanet/previewplan/entity/PlanCard.java +++ b/src/main/java/com/splanet/splanet/previewplan/entity/PlanCard.java @@ -29,8 +29,8 @@ public class PlanCard { private String startDate; private String endDate; - @TimeToLive(unit = TimeUnit.HOURS) - private Long expiration = 1L; + @TimeToLive + private Long expiration; public static String generateId() { return UUID.randomUUID().toString().split("-")[0]; diff --git a/src/main/java/com/splanet/splanet/previewplan/entity/PlanGroup.java b/src/main/java/com/splanet/splanet/previewplan/entity/PlanGroup.java index abf99d59..7708647b 100644 --- a/src/main/java/com/splanet/splanet/previewplan/entity/PlanGroup.java +++ b/src/main/java/com/splanet/splanet/previewplan/entity/PlanGroup.java @@ -25,6 +25,6 @@ public class PlanGroup { private String groupId; private Set planCardIds; - @TimeToLive(unit = TimeUnit.HOURS) - private Long expiration = 1L; + @TimeToLive + private Long expiration; } diff --git a/src/main/java/com/splanet/splanet/previewplan/entity/PreviewPlan.java b/src/main/java/com/splanet/splanet/previewplan/entity/PreviewPlan.java index 5665bbb4..f88525cd 100644 --- a/src/main/java/com/splanet/splanet/previewplan/entity/PreviewPlan.java +++ b/src/main/java/com/splanet/splanet/previewplan/entity/PreviewPlan.java @@ -1,6 +1,7 @@ package com.splanet.splanet.previewplan.entity; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import org.springframework.data.annotation.Id; @@ -13,6 +14,7 @@ @Getter @NoArgsConstructor @AllArgsConstructor +@Builder @RedisHash("previewPlan") public class PreviewPlan { @@ -21,6 +23,6 @@ public class PreviewPlan { private List groupIds; - @TimeToLive(unit = TimeUnit.HOURS) - private Long expiration = 1L; + @TimeToLive + private Long expiration; } diff --git a/src/main/java/com/splanet/splanet/previewplan/service/PreviewPlanService.java b/src/main/java/com/splanet/splanet/previewplan/service/PreviewPlanService.java index e8daaa8e..485767d4 100644 --- a/src/main/java/com/splanet/splanet/previewplan/service/PreviewPlanService.java +++ b/src/main/java/com/splanet/splanet/previewplan/service/PreviewPlanService.java @@ -24,6 +24,7 @@ @RequiredArgsConstructor public class PreviewPlanService { + private static final long EXPIRATION_TIME = 3600L; private static final String PLAN_GROUP_PREFIX = "planGroup:"; private static final String PLAN_CARD_PREFIX = "planCard:"; private static final int PLAN_CARD_PREFIX_LENGTH = PLAN_CARD_PREFIX.length(); @@ -94,6 +95,7 @@ private PlanCard buildPlanCard(String customKey, String deviceId, String groupId .description(planCardRequestDto.description()) .startDate(planCardRequestDto.startDate()) .endDate(planCardRequestDto.endDate()) + .expiration(EXPIRATION_TIME) .build(); } @@ -101,9 +103,11 @@ private void updatePlanGroup(String deviceId, String groupId, String cardId, boo String groupKey = generateGroupKey(deviceId, groupId); PlanGroup planGroup = planGroupRepository.findById(groupKey) .orElse(PlanGroup.builder() + .customKey(groupKey) .deviceId(deviceId) .groupId(groupId) .planCardIds(new HashSet<>()) + .expiration(EXPIRATION_TIME) .build()); if (add) { From c22b9a5389f5aef7d8088115fe54a04ba40d78c4 Mon Sep 17 00:00:00 2001 From: KimSongMok Date: Wed, 23 Oct 2024 20:23:15 +0900 Subject: [PATCH 19/46] feat: cd pipeline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cd pipeline 구축 --- .github/workflows/cd.yml | 49 +++++++++++++++ Dockerfile | 11 +++- docker-compose.yml | 78 ++++++++++++------------ src/main/resources/application-local.yml | 11 ++++ src/main/resources/application-prod.yml | 11 ++++ src/main/resources/application.yml | 31 +++------- 6 files changed, 128 insertions(+), 63 deletions(-) create mode 100644 .github/workflows/cd.yml create mode 100644 src/main/resources/application-local.yml create mode 100644 src/main/resources/application-prod.yml diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml new file mode 100644 index 00000000..b1748881 --- /dev/null +++ b/.github/workflows/cd.yml @@ -0,0 +1,49 @@ +name: CD Pipeline + +on: + push: + branches: + - develop # develop 브랜치에 merge될 때 트리거 + - 'weekly/**' + +jobs: + deploy: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Java 21 + uses: actions/setup-java@v3 + with: + distribution: 'temurin' + java-version: '21' + + - name: Build with Gradle # Spring Boot를 사용하는 경우 + run: ./gradlew build + + - name: Set up Docker + uses: docker/setup-buildx-action@v2 + + - name: Log in to Docker Hub + run: echo "${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}" | docker login -u "${{ secrets.DOCKER_HUB_USERNAME }}" --password-stdin + + - name: Set Image Tag + id: image_tag + run: echo "IMAGE_TAG=$(date +'%Y%m%d%H%M%S')" >> $GITHUB_ENV + + - name: Build and Push Docker image + run: docker buildx build --push --platform linux/amd64 -t kimsongmok/splanet:${{ env.IMAGE_TAG }} . + + - name: Deploy to EC2 + uses: appleboy/ssh-action@v0.1.6 + with: + host: ${{ secrets.EC2_HOST }} + username: ubuntu + key: ${{ secrets.EC2_SSH_KEY }} + script: | + sudo docker pull kimsongmok/splanet:${{ env.IMAGE_TAG }} # 새 이미지 풀링 + sudo docker stop splanet || true # 기존 컨테이너 중지 + sudo docker rm splanet || true # 기존 컨테이너 제거 + sudo docker run -d --name splanet -p 80:8080 kimsongmok/splanet:${{ env.IMAGE_TAG }} # 새 컨테이너 실행 \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index b7fe629f..84d6f0b2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,10 +6,17 @@ COPY build.gradle . COPY settings.gradle . COPY src src RUN chmod +x ./gradlew -RUN ./gradlew bootJar +# Gradle 빌드에서 프로필을 지정하여 실행 +RUN ./gradlew bootJar -Dspring.profiles.active=${SPRING_PROFILES_ACTIVE} + +# 런타임 단계 FROM eclipse-temurin:21 COPY --from=builder build/libs/*.jar app.jar +# 런타임에서도 동일하게 환경 변수 사용 +ENV SPRING_PROFILES_ACTIVE=prod + + ENTRYPOINT ["java", "-jar", "/app.jar"] -VOLUME /tmp +VOLUME /tmp \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 769880e8..c940c54b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,50 +1,50 @@ version: '3' services: - mysql: - container_name: mysql - image: mysql:8.0 - restart: always - environment: - MYSQL_USER: ${MYSQL_USER} - MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} - MYSQL_PASSWORD: ${MYSQL_PASSWORD} - MYSQL_DATABASE: ${MYSQL_DATABASE} - volumes: - - ./splanet-db/mysql:/var/lib/mysql - ports: - - 3306:3306 - networks: - - splanet - - redis: - container_name: redis - image: redis - ports: - - 6379:6379 - networks: - - splanet - - -# 개발할때는 주석처리하여 로컬로 개발합니다. -# springboot: -# container_name: springboot_splanet -# build: -# context: . -# dockerfile: Dockerfile +# mysql: +# container_name: mysql +# image: mysql:8.0 # restart: always -# depends_on: -# - mysql -# ports: -# - 8080:8080 # environment: -# SPRING_DATASOURCE_URL: ${SPRING_DATASOURCE_URL} -# SPRING_DATASOURCE_USERNAME: ${SPRING_DATASOURCE_USERNAME} -# SPRING_DATASOURCE_PASSWORD: ${SPRING_DATASOURCE_PASSWORD} -# SPRING_PROFILES_ACTIVE: ${SPRING_PROFILES_ACTIVE} +# MYSQL_USER: ${MYSQL_USER} +# MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} +# MYSQL_PASSWORD: ${MYSQL_PASSWORD} +# MYSQL_DATABASE: ${MYSQL_DATABASE} +# volumes: +# - ./splanet-db/mysql:/var/lib/mysql +# ports: +# - 3306:3306 +# networks: +# - splanet +# +# redis: +# container_name: redis +# image: redis +# ports: +# - 6379:6379 # networks: # - splanet + +# 개발할때는 주석처리하여 로컬로 개발합니다. + springboot: + container_name: springboot_splanet + build: + context: . + dockerfile: Dockerfile + restart: always + depends_on: + - mysql + ports: + - 8080:8080 + environment: + SPRING_DATASOURCE_URL: ${SPRING_DATASOURCE_URL} + SPRING_DATASOURCE_USERNAME: ${SPRING_DATASOURCE_USERNAME} + SPRING_DATASOURCE_PASSWORD: ${SPRING_DATASOURCE_PASSWORD} + SPRING_PROFILES_ACTIVE: ${SPRING_PROFILES_ACTIVE} + networks: + - splanet + networks: splanet: driver: bridge diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml new file mode 100644 index 00000000..44eb850a --- /dev/null +++ b/src/main/resources/application-local.yml @@ -0,0 +1,11 @@ +spring: + datasource: + url: jdbc:mysql://localhost:3306/${MYSQL_DATABASE} + username: ${MYSQL_USER} + password: ${MYSQL_PASSWORD} + driver-class-name: com.mysql.cj.jdbc.Driver + security: + oauth2: + redirect-url: http://localhost:8080/login/oauth2/code/kakao + config: + import: env.properties \ No newline at end of file diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml new file mode 100644 index 00000000..c1faa893 --- /dev/null +++ b/src/main/resources/application-prod.yml @@ -0,0 +1,11 @@ +spring: + datasource: + url: jdbc:mysql://${MYSQL_PROD_URL}:3306 + username: ${MYSQL_PROD_USER} + password: ${MYSQL_PROD_PASSWORD} + driver-class-name: com.mysql.cj.jdbc.Driver + security: + oauth2: + redirect-url: https://splanet.co.kr/login/oauth2/code/kakao + config: + import: env.properties \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index feb38a7c..d7af2286 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,9 +1,4 @@ spring: - datasource: - url: jdbc:mysql://localhost:3306/${MYSQL_DATABASE} - username: ${MYSQL_USER} - password: ${MYSQL_PASSWORD} - driver-class-name: com.mysql.cj.jdbc.Driver jpa: hibernate: ddl-auto: update @@ -14,30 +9,23 @@ spring: format_sql: true data: redis: - host: localhost # 로컬에서 실행하기 때문 + host: localhost port: 6379 - config: - import: env.properties security: oauth2: - redirect-url: ${REDIRECT_URL} client: - provider: - kakao: - authorization-uri: https://kauth.kakao.com/oauth/authorize - token-uri: https://kauth.kakao.com/oauth/token - user-info-uri: https://kapi.kakao.com/v2/user/me - user-name-attribute: id - registration: kakao: client-id: ${CLIENT_ID} client-secret: ${CLIENT_SECRET} - redirect-uri: http://localhost:8080/login/oauth2/code/kakao - client-authentication-method: client_secret_post + scope: profile + redirect-uri: ${REDIRECT_URL} authorization-grant-type: authorization_code - scope: profile_nickname, profile_image, account_email - client-name: Kakao + provider: + kakao: + authorization-uri: https://kauth.kakao.com/oauth/authorize + token-uri: https://kauth.kakao.com/oauth/token + user-info-uri: https://kapi.kakao.com/v2/user/me logging: level: @@ -45,5 +33,4 @@ logging: springdoc: swagger-ui: - path: /swagger - + path: /swagger \ No newline at end of file From 4f054a45d95670b2af1ba069110622ae6e09ea7f Mon Sep 17 00:00:00 2001 From: KimSongMok Date: Wed, 23 Oct 2024 20:47:18 +0900 Subject: [PATCH 20/46] =?UTF-8?q?refactor:=20jwt=ED=86=A0=ED=81=B0=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 받아오는 방식 수정 --- .../splanet/core/properties/JwtProperties.java | 14 ++++++++++++++ .../com/splanet/splanet/jwt/JwtTokenProvider.java | 10 ++++++---- src/main/resources/application.yml | 2 ++ 3 files changed, 22 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/splanet/splanet/core/properties/JwtProperties.java diff --git a/src/main/java/com/splanet/splanet/core/properties/JwtProperties.java b/src/main/java/com/splanet/splanet/core/properties/JwtProperties.java new file mode 100644 index 00000000..fb540351 --- /dev/null +++ b/src/main/java/com/splanet/splanet/core/properties/JwtProperties.java @@ -0,0 +1,14 @@ +package com.splanet.splanet.core.properties; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Getter +@Setter +@Configuration +@ConfigurationProperties(prefix = "spring.security.jwt") +public class JwtProperties { + private String secret; +} diff --git a/src/main/java/com/splanet/splanet/jwt/JwtTokenProvider.java b/src/main/java/com/splanet/splanet/jwt/JwtTokenProvider.java index 22c74a87..afa88a23 100644 --- a/src/main/java/com/splanet/splanet/jwt/JwtTokenProvider.java +++ b/src/main/java/com/splanet/splanet/jwt/JwtTokenProvider.java @@ -2,6 +2,7 @@ import com.splanet.splanet.core.exception.BusinessException; import com.splanet.splanet.core.exception.ErrorCode; +import com.splanet.splanet.core.properties.JwtProperties; import io.jsonwebtoken.*; import jakarta.annotation.PostConstruct; import jakarta.servlet.http.HttpServletRequest; @@ -18,16 +19,17 @@ public class JwtTokenProvider { private Key secretKey; - @Value("${JWT_SECRET}") - private String secret; - private static final long TOKEN_VALIDITY_IN_MILLISECONDS = 3600000; // 1시간 private static final long REFRESH_TOKEN_VALIDITY_IN_MILLISECONDS = 604800000; // 7일 + private final JwtProperties jwtProperties; + public JwtTokenProvider(JwtProperties jwtProperties) { + this.jwtProperties = jwtProperties; + } @PostConstruct protected void init() { - this.secretKey = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), SignatureAlgorithm.HS256.getJcaName()); + this.secretKey = new SecretKeySpec(jwtProperties.getSecret().getBytes(StandardCharsets.UTF_8), SignatureAlgorithm.HS256.getJcaName()); } public String createAccessToken(Long userId) { diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index d7af2286..358e139d 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -12,6 +12,8 @@ spring: host: localhost port: 6379 security: + jwt: + secret: ${JWT_SECRET} oauth2: client: registration: From e265a41b11f59e46007ee5d969d8aef4b356c8a4 Mon Sep 17 00:00:00 2001 From: KimSongMok Date: Wed, 23 Oct 2024 20:56:34 +0900 Subject: [PATCH 21/46] =?UTF-8?q?refactor:=20datasource=20=EB=AC=B8?= =?UTF-8?q?=EC=A0=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 3 +++ src/main/resources/application-prod.yml | 2 +- src/test/resources/application.yml | 0 3 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 src/test/resources/application.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f4dd0668..88e5c6b5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -89,10 +89,13 @@ jobs: - name: Build with Gradle run: ./gradlew build + env: + SPRING_PROFILES_ACTIVE: local - name: Run Tests run: ./gradlew test env: + SPRING_PROFILES_ACTIVE: local MYSQL_DATABASE: ${{ secrets.MYSQL_DATABASE }} MYSQL_USER: ${{ secrets.MYSQL_USER }} MYSQL_PASSWORD: ${{ secrets.MYSQL_PASSWORD }} diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index c1faa893..1e7f4b28 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -1,6 +1,6 @@ spring: datasource: - url: jdbc:mysql://${MYSQL_PROD_URL}:3306 + url: jdbc:mysql://${MYSQL_PROD_URL}:3306/${MYSQL_DATABASE} username: ${MYSQL_PROD_USER} password: ${MYSQL_PROD_PASSWORD} driver-class-name: com.mysql.cj.jdbc.Driver diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml new file mode 100644 index 00000000..e69de29b From 33643fa0f667fda2f153478532659be9a878c267 Mon Sep 17 00:00:00 2001 From: KimSongMok Date: Wed, 23 Oct 2024 21:43:26 +0900 Subject: [PATCH 22/46] =?UTF-8?q?refactor:=20test=EC=96=B4=EB=85=B8?= =?UTF-8?q?=ED=85=8C=EC=9D=B4=EC=85=98=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 3 +++ src/test/java/com/splanet/splanet/SplanetApplicationTests.java | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 88e5c6b5..26d7e5be 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -91,6 +91,9 @@ jobs: run: ./gradlew build env: SPRING_PROFILES_ACTIVE: local + MYSQL_DATABASE: ${{ secrets.MYSQL_DATABASE }} + MYSQL_USER: ${{ secrets.MYSQL_USER }} + MYSQL_PASSWORD: ${{ secrets.MYSQL_PASSWORD }} - name: Run Tests run: ./gradlew test diff --git a/src/test/java/com/splanet/splanet/SplanetApplicationTests.java b/src/test/java/com/splanet/splanet/SplanetApplicationTests.java index 6b6fb866..b40e2597 100644 --- a/src/test/java/com/splanet/splanet/SplanetApplicationTests.java +++ b/src/test/java/com/splanet/splanet/SplanetApplicationTests.java @@ -3,7 +3,7 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; -@SpringBootTest +// @SpringBootTest class SplanetApplicationTests { @Test From f2080b9b3169fa610414f59838e77cf0cca6f251 Mon Sep 17 00:00:00 2001 From: KimSongMok Date: Wed, 23 Oct 2024 21:51:28 +0900 Subject: [PATCH 23/46] =?UTF-8?q?refarctor:=20cd=20pipeline=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/cd.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index b1748881..0652c1d6 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -5,6 +5,10 @@ on: branches: - develop # develop 브랜치에 merge될 때 트리거 - 'weekly/**' + pull_request: + branches: + - develop + - 'weekly/**' jobs: deploy: From 8fb548024fc318b0be308be3116de52f1576b2e4 Mon Sep 17 00:00:00 2001 From: KimSongMok Date: Wed, 23 Oct 2024 22:18:31 +0900 Subject: [PATCH 24/46] =?UTF-8?q?refactor:=20prod.yml=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application-prod.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index 1e7f4b28..0f99cac2 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -8,4 +8,4 @@ spring: oauth2: redirect-url: https://splanet.co.kr/login/oauth2/code/kakao config: - import: env.properties \ No newline at end of file + import: optional:env.properties \ No newline at end of file From 7dadd3ff40cc73e9d53e5978478ad74cbbad9df1 Mon Sep 17 00:00:00 2001 From: KimSongMok Date: Wed, 23 Oct 2024 22:31:31 +0900 Subject: [PATCH 25/46] =?UTF-8?q?refactor:=20cd=20yml=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/cd.yml | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 0652c1d6..37b1cdf7 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -47,7 +47,17 @@ jobs: username: ubuntu key: ${{ secrets.EC2_SSH_KEY }} script: | - sudo docker pull kimsongmok/splanet:${{ env.IMAGE_TAG }} # 새 이미지 풀링 - sudo docker stop splanet || true # 기존 컨테이너 중지 - sudo docker rm splanet || true # 기존 컨테이너 제거 - sudo docker run -d --name splanet -p 80:8080 kimsongmok/splanet:${{ env.IMAGE_TAG }} # 새 컨테이너 실행 \ No newline at end of file + sudo docker pull kimsongmok/splanet:${{ env.IMAGE_TAG }} + sudo docker stop splanet || true + sudo docker rm splanet || true + sudo docker run -d --name splanet \ + -e MYSQL_PROD_URL=${{ secrets.MYSQL_PROD_URL }} \ + -e MYSQL_PROD_USER=${{ secrets.MYSQL_PROD_USER }} \ + -e MYSQL_PROD_PASSWORD=${{ secrets.MYSQL_PROD_PASSWORD }} \ + -e MYSQL_DATABASE=${{ secrets.MYSQL_DATABASE }} \ + -e CLIENT_ID=${{ secrets.CLIENT_ID }} \ + -e CLIENT_SECRET=${{ secrets.CLIENT_SECRET }} \ + -e JWT_SECRET=${{ secrets.JWT_SECRET }} \ + -e REDIRECT_URL=${{ secrets.REDIRECT_URL }} \ + -p 80:8080 --restart unless-stopped kimsongmok/splanet:${{ env.IMAGE_TAG }}\ + -v ./src/main/resources/env.properties \ No newline at end of file From 8c3d759416b21528742a797dda48d972e7a4a4b6 Mon Sep 17 00:00:00 2001 From: KimSongMok Date: Thu, 24 Oct 2024 01:51:19 +0900 Subject: [PATCH 26/46] =?UTF-8?q?refactor:=20application.yml=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 1 + src/main/resources/application.yml | 11 ++++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index d9924785..505230dd 100644 --- a/build.gradle +++ b/build.gradle @@ -32,6 +32,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'javax.xml.bind:jaxb-api:2.3.1' + implementation 'org.springframework.boot:spring-boot-starter-actuator' // implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6' implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2' implementation 'io.jsonwebtoken:jjwt:0.9.1' diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 358e139d..30a73b78 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -35,4 +35,13 @@ logging: springdoc: swagger-ui: - path: /swagger \ No newline at end of file + path: /swagger +management: + endpoints: + health: + show-details: always + web: + exposure: + include: health + security: + enabled: false \ No newline at end of file From 2917086dacc767cdaec424f86240bdf344106e2a Mon Sep 17 00:00:00 2001 From: kanguk Date: Thu, 24 Oct 2024 02:29:40 +0900 Subject: [PATCH 27/46] =?UTF-8?q?refactor:=20redis=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/cd.yml | 1 + src/main/resources/application-local.yml | 4 ++++ src/main/resources/application-prod.yml | 4 ++++ src/main/resources/application.yml | 5 +---- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 37b1cdf7..2d024340 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -51,6 +51,7 @@ jobs: sudo docker stop splanet || true sudo docker rm splanet || true sudo docker run -d --name splanet \ + --network splanet \ -e MYSQL_PROD_URL=${{ secrets.MYSQL_PROD_URL }} \ -e MYSQL_PROD_USER=${{ secrets.MYSQL_PROD_USER }} \ -e MYSQL_PROD_PASSWORD=${{ secrets.MYSQL_PROD_PASSWORD }} \ diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index 44eb850a..df8a51a6 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -1,4 +1,8 @@ spring: + data: + redis: + host: localhost + port: 6379 datasource: url: jdbc:mysql://localhost:3306/${MYSQL_DATABASE} username: ${MYSQL_USER} diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index 0f99cac2..2d08afe7 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -1,4 +1,8 @@ spring: + data: + redis: + host: redis + port: 6379 datasource: url: jdbc:mysql://${MYSQL_PROD_URL}:3306/${MYSQL_DATABASE} username: ${MYSQL_PROD_USER} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 30a73b78..41c7757d 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -7,10 +7,7 @@ spring: hibernate: dialect: org.hibernate.dialect.MySQLDialect format_sql: true - data: - redis: - host: localhost - port: 6379 + security: jwt: secret: ${JWT_SECRET} From c8e2008f4f3df6073019fa91ecf18cb84f3fd05d Mon Sep 17 00:00:00 2001 From: KimSongMok Date: Thu, 24 Oct 2024 02:56:51 +0900 Subject: [PATCH 28/46] =?UTF-8?q?refactor:=20application.yml=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit show details 위치 수정 --- src/main/resources/application.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 30a73b78..3e04690c 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -38,10 +38,9 @@ springdoc: path: /swagger management: endpoints: - health: - show-details: always web: exposure: include: health - security: - enabled: false \ No newline at end of file + endpoint: + health: + show-details: always \ No newline at end of file From 76cf902cf24cceaf11c184de2ee3810f8b1c25f2 Mon Sep 17 00:00:00 2001 From: KimSongMok Date: Thu, 24 Oct 2024 21:28:46 +0900 Subject: [PATCH 29/46] =?UTF-8?q?refator:=20cd=20healthy=20check=20api=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/controller/StatusCheckController.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 src/main/java/com/splanet/splanet/core/controller/StatusCheckController.java diff --git a/src/main/java/com/splanet/splanet/core/controller/StatusCheckController.java b/src/main/java/com/splanet/splanet/core/controller/StatusCheckController.java new file mode 100644 index 00000000..a4f71490 --- /dev/null +++ b/src/main/java/com/splanet/splanet/core/controller/StatusCheckController.java @@ -0,0 +1,15 @@ +package com.splanet.splanet.core.controller; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class StatusCheckController { + + @GetMapping("/actuator/health-check") + public ResponseEntity checkHealthStatus() { + return new ResponseEntity<>(HttpStatus.OK); + } +} \ No newline at end of file From cd2b182b21b42c42d20f6f5aaa1effae729256ff Mon Sep 17 00:00:00 2001 From: KimSongMok Date: Thu, 24 Oct 2024 21:34:30 +0900 Subject: [PATCH 30/46] =?UTF-8?q?refactor:=20cd=20healthy=20check=20api=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/controller/StatusCheckController.java | 15 --------------- 1 file changed, 15 deletions(-) delete mode 100644 src/main/java/com/splanet/splanet/core/controller/StatusCheckController.java diff --git a/src/main/java/com/splanet/splanet/core/controller/StatusCheckController.java b/src/main/java/com/splanet/splanet/core/controller/StatusCheckController.java deleted file mode 100644 index a4f71490..00000000 --- a/src/main/java/com/splanet/splanet/core/controller/StatusCheckController.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.splanet.splanet.core.controller; - -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RestController; - -@RestController -public class StatusCheckController { - - @GetMapping("/actuator/health-check") - public ResponseEntity checkHealthStatus() { - return new ResponseEntity<>(HttpStatus.OK); - } -} \ No newline at end of file From 63f38d2d9d55ec49dcb8ed857c0a0482e5e8b0d9 Mon Sep 17 00:00:00 2001 From: kanguk Date: Fri, 25 Oct 2024 00:46:09 +0900 Subject: [PATCH 31/46] =?UTF-8?q?refactor:=20=ED=94=84=EB=A1=9C=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EB=8D=94=20=EC=A0=95=ED=99=95=ED=95=98=EA=B2=8C=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 이전에 꼬여있던 부분 수정 --- .../core/properties/JwtProperties.java | 2 +- src/main/resources/application-local.yml | 16 ++++---- src/main/resources/application-prod.yml | 16 ++++---- src/main/resources/application.yml | 38 ++++++++++--------- 4 files changed, 39 insertions(+), 33 deletions(-) diff --git a/src/main/java/com/splanet/splanet/core/properties/JwtProperties.java b/src/main/java/com/splanet/splanet/core/properties/JwtProperties.java index fb540351..3c787400 100644 --- a/src/main/java/com/splanet/splanet/core/properties/JwtProperties.java +++ b/src/main/java/com/splanet/splanet/core/properties/JwtProperties.java @@ -8,7 +8,7 @@ @Getter @Setter @Configuration -@ConfigurationProperties(prefix = "spring.security.jwt") +@ConfigurationProperties(prefix = "jwt") public class JwtProperties { private String secret; } diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index df8a51a6..8eb1e6ac 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -1,15 +1,17 @@ spring: - data: - redis: - host: localhost - port: 6379 datasource: url: jdbc:mysql://localhost:3306/${MYSQL_DATABASE} username: ${MYSQL_USER} password: ${MYSQL_PASSWORD} driver-class-name: com.mysql.cj.jdbc.Driver + data: + redis: + host: localhost + port: 6379 security: oauth2: - redirect-url: http://localhost:8080/login/oauth2/code/kakao - config: - import: env.properties \ No newline at end of file + redirect-url: http://localhost:5173/oauth2/redirect + client: + registration: + kakao: + redirect-uri: http://localhost:8080/login/oauth2/code/kakao diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index 2d08afe7..4ff4c8ac 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -1,15 +1,17 @@ spring: - data: - redis: - host: redis - port: 6379 datasource: url: jdbc:mysql://${MYSQL_PROD_URL}:3306/${MYSQL_DATABASE} username: ${MYSQL_PROD_USER} password: ${MYSQL_PROD_PASSWORD} driver-class-name: com.mysql.cj.jdbc.Driver + data: + redis: + host: redis + port: 6379 security: oauth2: - redirect-url: https://splanet.co.kr/login/oauth2/code/kakao - config: - import: optional:env.properties \ No newline at end of file + redirect-url: https://splanet.co.kr/oauth2/redirect + client: + registration: + kakao: + redirect-uri: https://splanet.co.kr/login/oauth2/code/kakao diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 0796ff27..f79fd2b8 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -7,24 +7,26 @@ spring: hibernate: dialect: org.hibernate.dialect.MySQLDialect format_sql: true - + config: + import: optional:env.properties security: - jwt: - secret: ${JWT_SECRET} oauth2: client: - registration: - kakao: - client-id: ${CLIENT_ID} - client-secret: ${CLIENT_SECRET} - scope: profile - redirect-uri: ${REDIRECT_URL} - authorization-grant-type: authorization_code provider: kakao: authorization-uri: https://kauth.kakao.com/oauth/authorize token-uri: https://kauth.kakao.com/oauth/token user-info-uri: https://kapi.kakao.com/v2/user/me + user-name-attribute: id + registration: + kakao: + client-id: ${CLIENT_ID} + client-secret: ${CLIENT_SECRET} + redirect-uri: http://localhost:8080/login/oauth2/code/kakao + client-authentication-method: client_secret_post + authorization-grant-type: authorization_code + scope: profile_nickname, profile_image, account_email + client-name: Kakao logging: level: @@ -33,11 +35,11 @@ logging: springdoc: swagger-ui: path: /swagger -management: - endpoints: - web: - exposure: - include: health - endpoint: - health: - show-details: always \ No newline at end of file + +jwt: + secret: ${JWT_SECRET} + +clova: + speech: + client-secret: ${CLOVA_CLIENT_SECRET} + language: ko From 42a90614e51bcbbe4a430a397e6ab7b0244bacdf Mon Sep 17 00:00:00 2001 From: kanguk Date: Fri, 25 Oct 2024 01:45:10 +0900 Subject: [PATCH 32/46] =?UTF-8?q?refactor:=20gradle=20=EC=BA=90=EC=8B=B1?= =?UTF-8?q?=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit gradle 캐싱을 통해 빌드시간을 단축한다. --- .github/workflows/ci.yml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 26d7e5be..9f599e97 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,4 @@ -name: CI +name: CI (빌드 및 테스트) on: push: @@ -50,6 +50,16 @@ jobs: - name: Checkout Repository uses: actions/checkout@v2 + - name: Cache Gradle dependencies + uses: actions/cache@v3 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + gradle-${{ runner.os }}- + - name: Create env.properties run: | echo "MYSQL_DATABASE=${{ secrets.MYSQL_DATABASE }}" >> env.properties From ae13c81ca159f9b3753534e4323a1380dc2197f7 Mon Sep 17 00:00:00 2001 From: kanguk Date: Fri, 25 Oct 2024 02:12:34 +0900 Subject: [PATCH 33/46] =?UTF-8?q?refactor:=20=ED=99=98=EA=B2=BD=EB=B3=80?= =?UTF-8?q?=EC=88=98=20=EA=B4=80=EB=A6=AC=EA=B0=80=20=EC=9A=A9=EC=9D=B4?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit secret에 env.properties를 통째로 등록하여 관리할 수 있도록 한다. --- .github/workflows/cd.yml | 4 ---- .github/workflows/ci.yml | 45 ++++++++-------------------------------- 2 files changed, 9 insertions(+), 40 deletions(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 2d024340..a3669a45 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -1,10 +1,6 @@ name: CD Pipeline on: - push: - branches: - - develop # develop 브랜치에 merge될 때 트리거 - - 'weekly/**' pull_request: branches: - develop diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9f599e97..cebd9573 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,15 +16,6 @@ jobs: build-and-test: runs-on: ubuntu-22.04 - env: - MYSQL_DATABASE: ${{ secrets.MYSQL_DATABASE }} - MYSQL_USER: ${{ secrets.MYSQL_USER }} - MYSQL_PASSWORD: ${{ secrets.MYSQL_PASSWORD }} - MYSQL_ROOT_PASSWORD: ${{ secrets.MYSQL_ROOT_PASSWORD }} - CLIENT_ID: ${{ secrets.CLIENT_ID }} - CLIENT_SECRET: ${{ secrets.CLIENT_SECRET }} - JWT_SECRET: ${{ secrets.JWT_SECRET }} - services: mysql: image: mysql:8.0 @@ -60,17 +51,15 @@ jobs: restore-keys: | gradle-${{ runner.os }}- - - name: Create env.properties + - name: Decode env.properties from GitHub Secrets + run: | + echo "${{ secrets.ENV_FILE }}" | base64 --decode > ./src/main/resources/env.properties + + - name: Set environment variables from env.properties run: | - echo "MYSQL_DATABASE=${{ secrets.MYSQL_DATABASE }}" >> env.properties - echo "MYSQL_USER=${{ secrets.MYSQL_USER }}" >> env.properties - echo "MYSQL_PASSWORD=${{ secrets.MYSQL_PASSWORD }}" >> env.properties - echo "MYSQL_ROOT_PASSWORD: ${{ secrets.MYSQL_ROOT_PASSWORD }}" >> env.properties - echo "CLIENT_ID=${{ secrets.CLIENT_ID }}" >> env.properties - echo "CLIENT_SECRET=${{ secrets.CLIENT_SECRET }}" >> env.properties - echo "JWT_SECRET=${{ secrets.JWT_SECRET }}" >> env.properties - echo "REDIRECT_URL=${{ secrets.REDIRECT_URL }}" >> env.properties - cp env.properties ./src/main/resources + set -o allexport + source ./src/main/resources/env.properties + set +o allexport - name: Set up JDK 21 uses: actions/setup-java@v2 @@ -93,25 +82,9 @@ jobs: docker logs $(docker ps -q --filter name=mysql) exit 1 fi - env: - MYSQL_USER: ${{ secrets.MYSQL_USER }} - MYSQL_DATABASE: ${{ secrets.MYSQL_DATABASE }} - name: Build with Gradle run: ./gradlew build - env: - SPRING_PROFILES_ACTIVE: local - MYSQL_DATABASE: ${{ secrets.MYSQL_DATABASE }} - MYSQL_USER: ${{ secrets.MYSQL_USER }} - MYSQL_PASSWORD: ${{ secrets.MYSQL_PASSWORD }} - name: Run Tests - run: ./gradlew test - env: - SPRING_PROFILES_ACTIVE: local - MYSQL_DATABASE: ${{ secrets.MYSQL_DATABASE }} - MYSQL_USER: ${{ secrets.MYSQL_USER }} - MYSQL_PASSWORD: ${{ secrets.MYSQL_PASSWORD }} - CLIENT_ID: ${{ secrets.CLIENT_ID }} - CLIENT_SECRET: ${{ secrets.CLIENT_SECRET }} - JWT_SECRET: ${{ secrets.JWT_SECRET }} + run: ./gradlew test \ No newline at end of file From ca8a8792df020797b19fff6e74df96c0bc991edd Mon Sep 17 00:00:00 2001 From: kanguk Date: Fri, 25 Oct 2024 03:49:41 +0900 Subject: [PATCH 34/46] =?UTF-8?q?refactor:=20CD=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=ED=83=9C=EA=B7=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 커밋 해시값도 포함하여 커밋로그를 더 잘 추적할 수 있도록 한다. --- .github/workflows/cd.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index a3669a45..7c9d3627 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -20,9 +20,6 @@ jobs: distribution: 'temurin' java-version: '21' - - name: Build with Gradle # Spring Boot를 사용하는 경우 - run: ./gradlew build - - name: Set up Docker uses: docker/setup-buildx-action@v2 @@ -31,7 +28,7 @@ jobs: - name: Set Image Tag id: image_tag - run: echo "IMAGE_TAG=$(date +'%Y%m%d%H%M%S')" >> $GITHUB_ENV + run: echo "IMAGE_TAG=$(date +'%Y-%m-%d_%H-%M-%S')-$(echo ${{ github.sha }} | cut -c1-8)" >> $GITHUB_ENV - name: Build and Push Docker image run: docker buildx build --push --platform linux/amd64 -t kimsongmok/splanet:${{ env.IMAGE_TAG }} . From f291783d014b86b16e43f1acd41f54d84518f2d8 Mon Sep 17 00:00:00 2001 From: kanguk Date: Fri, 25 Oct 2024 04:00:32 +0900 Subject: [PATCH 35/46] =?UTF-8?q?refactor:=20=ED=99=98=EA=B2=BD=EB=B3=80?= =?UTF-8?q?=EC=88=98=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=EC=A0=95=EB=A6=AC=20=EB=A1=9C=EC=A7=81=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 환경변수를 용이하게 관리할 수 있도록 수정하고, 오래된 이미지를 삭제하는 스크립트를 추가로 작성한다. --- .github/workflows/cd.yml | 48 +++++++++++++++++++++++++++++++--------- 1 file changed, 37 insertions(+), 11 deletions(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 7c9d3627..3996241e 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -30,6 +30,19 @@ jobs: id: image_tag run: echo "IMAGE_TAG=$(date +'%Y-%m-%d_%H-%M-%S')-$(echo ${{ github.sha }} | cut -c1-8)" >> $GITHUB_ENV + - name: Decode env.properties from GitHub Secrets + run: | + echo "${{ secrets.ENV_FILE }}" | base64 --decode > ./env.properties + + - name: Transfer env.properties to EC2 + uses: appleboy/scp-action@v0.1.3 + with: + host: ${{ secrets.EC2_HOST }} + username: ubuntu + key: ${{ secrets.EC2_SSH_KEY }} + source: "./env.properties" + target: "/home/ubuntu/" + - name: Build and Push Docker image run: docker buildx build --push --platform linux/amd64 -t kimsongmok/splanet:${{ env.IMAGE_TAG }} . @@ -43,15 +56,28 @@ jobs: sudo docker pull kimsongmok/splanet:${{ env.IMAGE_TAG }} sudo docker stop splanet || true sudo docker rm splanet || true + sudo docker network inspect splanet >/dev/null 2>&1 || sudo docker network create splanet sudo docker run -d --name splanet \ - --network splanet \ - -e MYSQL_PROD_URL=${{ secrets.MYSQL_PROD_URL }} \ - -e MYSQL_PROD_USER=${{ secrets.MYSQL_PROD_USER }} \ - -e MYSQL_PROD_PASSWORD=${{ secrets.MYSQL_PROD_PASSWORD }} \ - -e MYSQL_DATABASE=${{ secrets.MYSQL_DATABASE }} \ - -e CLIENT_ID=${{ secrets.CLIENT_ID }} \ - -e CLIENT_SECRET=${{ secrets.CLIENT_SECRET }} \ - -e JWT_SECRET=${{ secrets.JWT_SECRET }} \ - -e REDIRECT_URL=${{ secrets.REDIRECT_URL }} \ - -p 80:8080 --restart unless-stopped kimsongmok/splanet:${{ env.IMAGE_TAG }}\ - -v ./src/main/resources/env.properties \ No newline at end of file + --network splanet \ + --env-file /home/ubuntu/env.properties \ + -p 80:8080 --restart unless-stopped kimsongmok/splanet:${{ env.IMAGE_TAG }} + + - name: Check Docker container status + uses: appleboy/ssh-action@v0.1.6 + with: + host: ${{ secrets.EC2_HOST }} + username: ubuntu + key: ${{ secrets.EC2_SSH_KEY }} + script: | + sudo docker ps -a + sudo docker logs splanet + + - name: Clean up old Docker images + uses: appleboy/ssh-action@v0.1.6 + with: + host: ${{ secrets.EC2_HOST }} + username: ubuntu + key: ${{ secrets.EC2_SSH_KEY }} + script: | + docker image ls --format "{{.ID}} {{.Repository}}:{{.Tag}}" | grep 'kimsongmok/splanet' | tail -n +4 | awk '{print $1}' | xargs docker rmi -f + sudo docker system prune -f From 348983b7bc05e516cddf429bae719a64796c06e5 Mon Sep 17 00:00:00 2001 From: kanguk Date: Fri, 25 Oct 2024 04:36:45 +0900 Subject: [PATCH 36/46] =?UTF-8?q?refactor:=20cd=20=ED=8A=B8=EB=A6=AC?= =?UTF-8?q?=EA=B1=B0=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit develop에 merge가 되면 자동배포를 실시하도록 트리거를 수정한다. --- .github/workflows/cd.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 3996241e..11f0d9d0 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -1,11 +1,9 @@ -name: CD Pipeline +name: CD (자동배포) on: - pull_request: + push: branches: - develop - - 'weekly/**' - jobs: deploy: runs-on: ubuntu-latest From 6b1abdd9c143cb79c84c1724b95f1cf9b5fc53c6 Mon Sep 17 00:00:00 2001 From: kanguk Date: Fri, 18 Oct 2024 19:03:10 +0900 Subject: [PATCH 37/46] =?UTF-8?q?feat:=20STT=20=EC=B4=88=EA=B8=B0=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 파일 업로드 -> STT 실행의 기본 기능을 구현한다. --- build.gradle | 2 +- .../core/properties/ClovaProperties.java | 17 ++++++ .../SpeechRecognitionController.java | 24 ++++++++ .../stt/service/ClovaSpeechService.java | 7 +++ .../stt/service/ClovaSpeechServiceImpl.java | 56 +++++++++++++++++++ 5 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/splanet/splanet/core/properties/ClovaProperties.java create mode 100644 src/main/java/com/splanet/splanet/stt/controller/SpeechRecognitionController.java create mode 100644 src/main/java/com/splanet/splanet/stt/service/ClovaSpeechService.java create mode 100644 src/main/java/com/splanet/splanet/stt/service/ClovaSpeechServiceImpl.java diff --git a/build.gradle b/build.gradle index 505230dd..f3547be9 100644 --- a/build.gradle +++ b/build.gradle @@ -38,7 +38,7 @@ dependencies { implementation 'io.jsonwebtoken:jjwt:0.9.1' implementation 'org.hibernate.validator:hibernate-validator:8.0.0.Final' implementation 'jakarta.validation:jakarta.validation-api:3.0.2' - implementation 'org.springframework.boot:spring-boot-starter-data-redis' + implementation 'org.apache.httpcomponents.client5:httpclient5:5.2.1' compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.mysql:mysql-connector-j' annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor' diff --git a/src/main/java/com/splanet/splanet/core/properties/ClovaProperties.java b/src/main/java/com/splanet/splanet/core/properties/ClovaProperties.java new file mode 100644 index 00000000..e4421cef --- /dev/null +++ b/src/main/java/com/splanet/splanet/core/properties/ClovaProperties.java @@ -0,0 +1,17 @@ +package com.splanet.splanet.core.properties; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Configuration +@ConfigurationProperties(prefix = "clova") +public class ClovaProperties { + private String clientId; + private String clientSecret; + private String url; + private String language; +} diff --git a/src/main/java/com/splanet/splanet/stt/controller/SpeechRecognitionController.java b/src/main/java/com/splanet/splanet/stt/controller/SpeechRecognitionController.java new file mode 100644 index 00000000..e10f6b0c --- /dev/null +++ b/src/main/java/com/splanet/splanet/stt/controller/SpeechRecognitionController.java @@ -0,0 +1,24 @@ +package com.splanet.splanet.stt.controller; + +import com.splanet.splanet.stt.service.ClovaSpeechService; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/stt") +public class SpeechRecognitionController { + + private final ClovaSpeechService clovaSpeechService; + + public SpeechRecognitionController(ClovaSpeechService clovaSpeechService) { + this.clovaSpeechService = clovaSpeechService; + } + + @PostMapping(consumes = "multipart/form-data") + public String recognizeSpeech(@RequestParam("file") MultipartFile file) { + return clovaSpeechService.recognize(file); + } +} diff --git a/src/main/java/com/splanet/splanet/stt/service/ClovaSpeechService.java b/src/main/java/com/splanet/splanet/stt/service/ClovaSpeechService.java new file mode 100644 index 00000000..fd9b2e8f --- /dev/null +++ b/src/main/java/com/splanet/splanet/stt/service/ClovaSpeechService.java @@ -0,0 +1,7 @@ +package com.splanet.splanet.stt.service; + +import org.springframework.web.multipart.MultipartFile; + +public interface ClovaSpeechService { + String recognize(MultipartFile file); +} diff --git a/src/main/java/com/splanet/splanet/stt/service/ClovaSpeechServiceImpl.java b/src/main/java/com/splanet/splanet/stt/service/ClovaSpeechServiceImpl.java new file mode 100644 index 00000000..294d7a92 --- /dev/null +++ b/src/main/java/com/splanet/splanet/stt/service/ClovaSpeechServiceImpl.java @@ -0,0 +1,56 @@ +package com.splanet.splanet.stt.service; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.splanet.splanet.core.exception.BusinessException; +import com.splanet.splanet.core.exception.ErrorCode; +import com.splanet.splanet.core.properties.ClovaProperties; +import org.apache.hc.client5.http.classic.methods.HttpPost; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.client5.http.impl.classic.HttpClients; +import org.apache.hc.core5.http.ContentType; +import org.apache.hc.core5.http.io.entity.ByteArrayEntity; +import org.apache.hc.core5.http.io.entity.EntityUtils; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +@Service +public class ClovaSpeechServiceImpl implements ClovaSpeechService { + + private final ClovaProperties clovaProperties; + + public ClovaSpeechServiceImpl(ClovaProperties clovaProperties) { + this.clovaProperties = clovaProperties; + } + + @Override + public String recognize(MultipartFile file) { + String apiURL = clovaProperties.getUrl() + "?lang=" + clovaProperties.getLanguage(); // 언어 설정 반영 + try (CloseableHttpClient httpClient = HttpClients.createDefault()) { + byte[] audioBytes = file.getBytes(); + + HttpPost httpPost = new HttpPost(apiURL); + httpPost.addHeader("Content-Type", "application/octet-stream"); + httpPost.addHeader("X-NCP-APIGW-API-KEY-ID", clovaProperties.getClientId()); + httpPost.addHeader("X-NCP-APIGW-API-KEY", clovaProperties.getClientSecret()); + + ByteArrayEntity byteArrayEntity = new ByteArrayEntity(audioBytes, ContentType.APPLICATION_OCTET_STREAM); + httpPost.setEntity(byteArrayEntity); + + return httpClient.execute(httpPost, response -> { + int statusCode = response.getCode(); + String responseBody = EntityUtils.toString(response.getEntity(), "UTF-8"); + + if (statusCode == 200) { + ObjectMapper objectMapper = new ObjectMapper(); + JsonNode rootNode = objectMapper.readTree(responseBody); + return rootNode.path("text").asText(); + } else { + throw new BusinessException(ErrorCode.INVALID_INPUT_VALUE, "CLOVA Speech API 호출 실패: " + responseBody); + } + }); + } catch (Exception e) { + throw new BusinessException(ErrorCode.INVALID_INPUT_VALUE, "오디오 파일 처리 중 오류 발생: " + e.getMessage()); + } + } +} \ No newline at end of file From bf9c12f9870b83ef591e2747a8b334ac3f2bb121 Mon Sep 17 00:00:00 2001 From: kanguk Date: Fri, 18 Oct 2024 22:57:23 +0900 Subject: [PATCH 38/46] =?UTF-8?q?feat:=20WebSocket=20=EC=9D=84=20=ED=99=9C?= =?UTF-8?q?=EC=9A=A9=ED=95=98=EC=97=AC=20=EC=8B=A4=EC=8B=9C=EA=B0=84?= =?UTF-8?q?=EC=B2=98=EB=9F=BC=20=EB=B3=B4=EC=9D=B4=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=ED=95=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 하지만 문제가 있음 (임시커밋) --- build.gradle | 1 + .../com/splanet/splanet/config/WebConfig.java | 16 ++++ .../splanet/config/WebSocketConfig.java | 34 ++++++++ .../core/handler/SpeechWebSocketHandler.java | 78 +++++++++++++++++++ .../splanet/jwt/JwtAuthenticationFilter.java | 2 +- .../SpeechRecognitionController.java | 53 +++++++------ .../stt/service/ClovaSpeechService.java | 2 +- .../stt/service/ClovaSpeechServiceImpl.java | 10 +-- 8 files changed, 164 insertions(+), 32 deletions(-) create mode 100644 src/main/java/com/splanet/splanet/config/WebConfig.java create mode 100644 src/main/java/com/splanet/splanet/config/WebSocketConfig.java create mode 100644 src/main/java/com/splanet/splanet/core/handler/SpeechWebSocketHandler.java diff --git a/build.gradle b/build.gradle index f3547be9..646cb796 100644 --- a/build.gradle +++ b/build.gradle @@ -39,6 +39,7 @@ dependencies { implementation 'org.hibernate.validator:hibernate-validator:8.0.0.Final' implementation 'jakarta.validation:jakarta.validation-api:3.0.2' implementation 'org.apache.httpcomponents.client5:httpclient5:5.2.1' + implementation 'org.springframework.boot:spring-boot-starter-websocket' compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.mysql:mysql-connector-j' annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor' diff --git a/src/main/java/com/splanet/splanet/config/WebConfig.java b/src/main/java/com/splanet/splanet/config/WebConfig.java new file mode 100644 index 00000000..ee84bcee --- /dev/null +++ b/src/main/java/com/splanet/splanet/config/WebConfig.java @@ -0,0 +1,16 @@ +package com.splanet.splanet.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class WebConfig implements WebMvcConfigurer { + + @Override + public void addCorsMappings(CorsRegistry registry) { //인터페이스 WebMvcConfigurer 상속 + registry.addMapping("/**") //모든 경로를 허용해줄것이므로 + .allowedOrigins("*") //리소스 공유 허락할 origin 지정 + .allowedMethods("*"); //모든 메소드를 허용 + } +} \ No newline at end of file diff --git a/src/main/java/com/splanet/splanet/config/WebSocketConfig.java b/src/main/java/com/splanet/splanet/config/WebSocketConfig.java new file mode 100644 index 00000000..d5688e67 --- /dev/null +++ b/src/main/java/com/splanet/splanet/config/WebSocketConfig.java @@ -0,0 +1,34 @@ +package com.splanet.splanet.config; + +import com.splanet.splanet.core.handler.SpeechWebSocketHandler; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.socket.config.annotation.EnableWebSocket; +import org.springframework.web.socket.config.annotation.WebSocketConfigurer; +import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; +import org.springframework.web.socket.server.standard.ServletServerContainerFactoryBean; + +@Configuration +@EnableWebSocket +public class WebSocketConfig implements WebSocketConfigurer { + + private final SpeechWebSocketHandler speechWebSocketHandler; + + public WebSocketConfig(SpeechWebSocketHandler speechWebSocketHandler) { + this.speechWebSocketHandler = speechWebSocketHandler; + } + + @Override + public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { + registry.addHandler(speechWebSocketHandler, "/ws/stt") + .setAllowedOrigins("*"); + } + + @Bean + public ServletServerContainerFactoryBean configureWebSocketContainer() { + ServletServerContainerFactoryBean factory = new ServletServerContainerFactoryBean(); + factory.setMaxBinaryMessageBufferSize(256 * 1024); //바이너리 버퍼 크기 지정 16KB + factory.setMaxTextMessageBufferSize(256 * 1024); //텍스트 버퍼 크기 지정 16KB + return factory; + } +} \ No newline at end of file diff --git a/src/main/java/com/splanet/splanet/core/handler/SpeechWebSocketHandler.java b/src/main/java/com/splanet/splanet/core/handler/SpeechWebSocketHandler.java new file mode 100644 index 00000000..f5d5ab05 --- /dev/null +++ b/src/main/java/com/splanet/splanet/core/handler/SpeechWebSocketHandler.java @@ -0,0 +1,78 @@ +package com.splanet.splanet.core.handler; + +import com.splanet.splanet.stt.service.ClovaSpeechService; +import org.springframework.web.socket.BinaryMessage; +import org.springframework.web.socket.TextMessage; +import org.springframework.web.socket.WebSocketSession; +import org.springframework.web.socket.handler.BinaryWebSocketHandler; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; + +@Component +public class SpeechWebSocketHandler extends BinaryWebSocketHandler { + + private final ClovaSpeechService clovaSpeechService; + private List audioDataBuffer = new ArrayList<>(); + private static final int MINIMUM_AUDIO_SIZE = 64000; // 최소 데이터 크기를 96KB로 설정 (약 3초 분량) + + public SpeechWebSocketHandler(ClovaSpeechService clovaSpeechService) { + this.clovaSpeechService = clovaSpeechService; + } + + @Override + protected synchronized void handleBinaryMessage(WebSocketSession session, BinaryMessage message) throws Exception { + session.setBinaryMessageSizeLimit(256 * 1024); // 메시지 크기 제한을 256KB로 설정 + byte[] audioData = message.getPayload().array(); + + // 오디오 데이터를 버퍼에 추가 + audioDataBuffer.add(audioData); + + // 누적된 오디오 데이터 크기 계산 + int totalSize = audioDataBuffer.stream().mapToInt(arr -> arr.length).sum(); + + // 현재 누적된 데이터 크기를 로그로 출력 + System.out.println("현재 누적된 데이터 크기: " + totalSize + " bytes"); + + // 오디오 데이터가 충분히 쌓였을 때만 CLOVA API로 전송 + if (totalSize >= MINIMUM_AUDIO_SIZE) { + byte[] fullAudioData = mergeAudioData(); + try { + // CLOVA API로 전송 + String transcript = clovaSpeechService.recognize(fullAudioData); + session.sendMessage(new TextMessage(transcript)); + // 인식에 성공했으므로 버퍼를 초기화 + audioDataBuffer.clear(); + System.out.println("인식 성공: 버퍼를 초기화합니다."); + } catch (Exception e) { + e.printStackTrace(); + // STT007 오류 발생 시 버퍼를 유지하고 데이터 수집 계속 + if (e.getMessage().contains("STT007")) { + System.err.println("오류 발생: STT007 - 데이터가 너무 작습니다. 더 많은 데이터를 수집 중..."); + // 버퍼를 유지하여 다음 데이터를 기다립니다. + } else { + // 다른 오류 발생 시 버퍼를 초기화하고 오류 메시지 전송 + audioDataBuffer.clear(); + session.sendMessage(new TextMessage("오류 발생: " + e.getMessage())); + System.err.println("오류 발생: " + e.getMessage() + " - 버퍼를 초기화합니다."); + } + } + } else { + // 아직 데이터가 충분하지 않으면 아무 작업도 하지 않음 + System.out.println("데이터가 아직 충분하지 않음"); + } + } + + // 누적된 오디오 데이터를 병합하는 메서드 + private byte[] mergeAudioData() { + int totalLength = audioDataBuffer.stream().mapToInt(arr -> arr.length).sum(); + byte[] mergedData = new byte[totalLength]; + int currentIndex = 0; + for (byte[] data : audioDataBuffer) { + System.arraycopy(data, 0, mergedData, currentIndex, data.length); + currentIndex += data.length; + } + return mergedData; + } +} diff --git a/src/main/java/com/splanet/splanet/jwt/JwtAuthenticationFilter.java b/src/main/java/com/splanet/splanet/jwt/JwtAuthenticationFilter.java index 68f0a513..26142f8b 100644 --- a/src/main/java/com/splanet/splanet/jwt/JwtAuthenticationFilter.java +++ b/src/main/java/com/splanet/splanet/jwt/JwtAuthenticationFilter.java @@ -76,7 +76,7 @@ private boolean isApiPath(String requestURI) { } private boolean isExemptedPath(String requestURI) { - return requestURI.equals("/api/users/create") || requestURI.startsWith("/api/token"); + return requestURI.equals("/api/users/create") || requestURI.startsWith("/api/token") || requestURI.startsWith("/api/stt"); } private void sendErrorResponse(HttpServletResponse response, int status, String message) throws IOException { diff --git a/src/main/java/com/splanet/splanet/stt/controller/SpeechRecognitionController.java b/src/main/java/com/splanet/splanet/stt/controller/SpeechRecognitionController.java index e10f6b0c..0c7d406a 100644 --- a/src/main/java/com/splanet/splanet/stt/controller/SpeechRecognitionController.java +++ b/src/main/java/com/splanet/splanet/stt/controller/SpeechRecognitionController.java @@ -1,24 +1,29 @@ -package com.splanet.splanet.stt.controller; - -import com.splanet.splanet.stt.service.ClovaSpeechService; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.multipart.MultipartFile; -import org.springframework.web.bind.annotation.RestController; - -@RestController -@RequestMapping("/api/stt") -public class SpeechRecognitionController { - - private final ClovaSpeechService clovaSpeechService; - - public SpeechRecognitionController(ClovaSpeechService clovaSpeechService) { - this.clovaSpeechService = clovaSpeechService; - } - - @PostMapping(consumes = "multipart/form-data") - public String recognizeSpeech(@RequestParam("file") MultipartFile file) { - return clovaSpeechService.recognize(file); - } -} +//package com.splanet.splanet.stt.controller; +// +//import com.splanet.splanet.stt.service.ClovaSpeechService; +//import org.springframework.stereotype.Controller; +//import org.springframework.web.bind.annotation.PostMapping; +//import org.springframework.web.bind.annotation.RequestMapping; +//import org.springframework.web.bind.annotation.RequestParam; +//import org.springframework.web.multipart.MultipartFile; +//import org.springframework.web.bind.annotation.RestController; +// +//@Controller +//@RequestMapping("/api/stt") +//public class SpeechRecognitionController { +// +// private final ClovaSpeechService clovaSpeechService; +// +// public SpeechRecognitionController(ClovaSpeechService clovaSpeechService) { +// this.clovaSpeechService = clovaSpeechService; +// } +// +// @PostMapping(consumes = "multipart/form-data") +// public String recognizeSpeech(@RequestParam("file") MultipartFile file) { +// try { +// return clovaSpeechService.recognize(file.getBytes()); +// } catch (Exception e) { +// throw new RuntimeException("파일 처리 중 오류 발생: " + e.getMessage()); +// } +// } +//} diff --git a/src/main/java/com/splanet/splanet/stt/service/ClovaSpeechService.java b/src/main/java/com/splanet/splanet/stt/service/ClovaSpeechService.java index fd9b2e8f..af1551d6 100644 --- a/src/main/java/com/splanet/splanet/stt/service/ClovaSpeechService.java +++ b/src/main/java/com/splanet/splanet/stt/service/ClovaSpeechService.java @@ -3,5 +3,5 @@ import org.springframework.web.multipart.MultipartFile; public interface ClovaSpeechService { - String recognize(MultipartFile file); + String recognize(byte[] audioBytes); } diff --git a/src/main/java/com/splanet/splanet/stt/service/ClovaSpeechServiceImpl.java b/src/main/java/com/splanet/splanet/stt/service/ClovaSpeechServiceImpl.java index 294d7a92..f08bf8fe 100644 --- a/src/main/java/com/splanet/splanet/stt/service/ClovaSpeechServiceImpl.java +++ b/src/main/java/com/splanet/splanet/stt/service/ClovaSpeechServiceImpl.java @@ -24,11 +24,9 @@ public ClovaSpeechServiceImpl(ClovaProperties clovaProperties) { } @Override - public String recognize(MultipartFile file) { - String apiURL = clovaProperties.getUrl() + "?lang=" + clovaProperties.getLanguage(); // 언어 설정 반영 + public String recognize(byte[] audioBytes) { + String apiURL = clovaProperties.getUrl() + "?lang=" + clovaProperties.getLanguage(); try (CloseableHttpClient httpClient = HttpClients.createDefault()) { - byte[] audioBytes = file.getBytes(); - HttpPost httpPost = new HttpPost(apiURL); httpPost.addHeader("Content-Type", "application/octet-stream"); httpPost.addHeader("X-NCP-APIGW-API-KEY-ID", clovaProperties.getClientId()); @@ -46,11 +44,11 @@ public String recognize(MultipartFile file) { JsonNode rootNode = objectMapper.readTree(responseBody); return rootNode.path("text").asText(); } else { - throw new BusinessException(ErrorCode.INVALID_INPUT_VALUE, "CLOVA Speech API 호출 실패: " + responseBody); + throw new RuntimeException("CLOVA Speech API 호출 실패: " + responseBody); } }); } catch (Exception e) { - throw new BusinessException(ErrorCode.INVALID_INPUT_VALUE, "오디오 파일 처리 중 오류 발생: " + e.getMessage()); + throw new RuntimeException("오디오 파일 처리 중 오류 발생: " + e.getMessage()); } } } \ No newline at end of file From b0d810e6fc0230c8b333c5cda16900353e9202c9 Mon Sep 17 00:00:00 2001 From: kanguk Date: Sat, 19 Oct 2024 02:44:20 +0900 Subject: [PATCH 39/46] =?UTF-8?q?feat:=20=EC=8B=A4=EC=8B=9C=EA=B0=84=20?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=EB=A6=AC=EB=B0=8D=20API=EB=A5=BC=20=ED=99=9C?= =?UTF-8?q?=EC=9A=A9=ED=95=98=EC=97=AC=20STT=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 기존 CLOVA Speech Recognition 에서 CLOVA Speech 실시간 스트리밍 API를 활용하여 실시간 처리를 가능하도록 수정한다. --- build.gradle | 39 ++++++ .../splanet/config/WebSocketConfig.java | 10 -- .../core/handler/SpeechWebSocketHandler.java | 124 +++++++++++------- .../core/properties/ClovaProperties.java | 4 +- .../SpeechRecognitionController.java | 29 ---- .../stt/service/ClovaSpeechGrpcService.java | 89 +++++++++++++ .../stt/service/ClovaSpeechService.java | 6 +- .../stt/service/ClovaSpeechServiceImpl.java | 54 -------- src/main/proto/nest.proto | 34 +++++ 9 files changed, 240 insertions(+), 149 deletions(-) delete mode 100644 src/main/java/com/splanet/splanet/stt/controller/SpeechRecognitionController.java create mode 100644 src/main/java/com/splanet/splanet/stt/service/ClovaSpeechGrpcService.java delete mode 100644 src/main/java/com/splanet/splanet/stt/service/ClovaSpeechServiceImpl.java create mode 100644 src/main/proto/nest.proto diff --git a/build.gradle b/build.gradle index 646cb796..f02fed08 100644 --- a/build.gradle +++ b/build.gradle @@ -2,6 +2,8 @@ plugins { id 'java' id 'org.springframework.boot' version '3.3.3' id 'io.spring.dependency-management' version '1.1.6' + id 'com.google.protobuf' version '0.9.4' + } group = 'com.splanet' @@ -40,6 +42,17 @@ dependencies { implementation 'jakarta.validation:jakarta.validation-api:3.0.2' implementation 'org.apache.httpcomponents.client5:httpclient5:5.2.1' implementation 'org.springframework.boot:spring-boot-starter-websocket' + // gRPC 및 Protocol Buffers 의존성 + implementation 'io.grpc:grpc-netty-shaded:1.56.1' + implementation 'io.grpc:grpc-protobuf:1.56.1' + implementation 'io.grpc:grpc-stub:1.56.1' + implementation 'com.google.protobuf:protobuf-java:3.23.4' + + // gRPC 관련 필요한 의존성 + implementation 'javax.annotation:javax.annotation-api:1.3.2' + implementation 'com.google.code.gson:gson:2.8.9' + + compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.mysql:mysql-connector-j' annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor' @@ -53,3 +66,29 @@ dependencies { tasks.named('test') { useJUnitPlatform() } + +protobuf { + protoc { + artifact = 'com.google.protobuf:protoc:3.23.4' + } + plugins { + grpc { + artifact = 'io.grpc:protoc-gen-grpc-java:1.66.0' + } + } + generateProtoTasks { + all().forEach { task -> + task.plugins { + grpc {} + } + } + } +} + +sourceSets { + main { + java { + srcDirs 'build/generated/source/proto/main/java', 'build/generated/source/proto/main/grpc' + } + } +} \ No newline at end of file diff --git a/src/main/java/com/splanet/splanet/config/WebSocketConfig.java b/src/main/java/com/splanet/splanet/config/WebSocketConfig.java index d5688e67..db192738 100644 --- a/src/main/java/com/splanet/splanet/config/WebSocketConfig.java +++ b/src/main/java/com/splanet/splanet/config/WebSocketConfig.java @@ -1,12 +1,10 @@ package com.splanet.splanet.config; import com.splanet.splanet.core.handler.SpeechWebSocketHandler; -import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.socket.config.annotation.EnableWebSocket; import org.springframework.web.socket.config.annotation.WebSocketConfigurer; import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; -import org.springframework.web.socket.server.standard.ServletServerContainerFactoryBean; @Configuration @EnableWebSocket @@ -23,12 +21,4 @@ public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(speechWebSocketHandler, "/ws/stt") .setAllowedOrigins("*"); } - - @Bean - public ServletServerContainerFactoryBean configureWebSocketContainer() { - ServletServerContainerFactoryBean factory = new ServletServerContainerFactoryBean(); - factory.setMaxBinaryMessageBufferSize(256 * 1024); //바이너리 버퍼 크기 지정 16KB - factory.setMaxTextMessageBufferSize(256 * 1024); //텍스트 버퍼 크기 지정 16KB - return factory; - } } \ No newline at end of file diff --git a/src/main/java/com/splanet/splanet/core/handler/SpeechWebSocketHandler.java b/src/main/java/com/splanet/splanet/core/handler/SpeechWebSocketHandler.java index f5d5ab05..f1710902 100644 --- a/src/main/java/com/splanet/splanet/core/handler/SpeechWebSocketHandler.java +++ b/src/main/java/com/splanet/splanet/core/handler/SpeechWebSocketHandler.java @@ -1,78 +1,100 @@ package com.splanet.splanet.core.handler; -import com.splanet.splanet.stt.service.ClovaSpeechService; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.protobuf.ByteString; +import com.nbp.cdncp.nest.grpc.proto.v1.NestResponse; +import com.splanet.splanet.stt.service.ClovaSpeechGrpcService; +import io.grpc.stub.StreamObserver; +import org.springframework.stereotype.Component; import org.springframework.web.socket.BinaryMessage; +import org.springframework.web.socket.CloseStatus; import org.springframework.web.socket.TextMessage; import org.springframework.web.socket.WebSocketSession; import org.springframework.web.socket.handler.BinaryWebSocketHandler; -import org.springframework.stereotype.Component; -import java.util.ArrayList; -import java.util.List; +import java.io.IOException; +import java.util.concurrent.ConcurrentHashMap; +import java.util.Map; @Component public class SpeechWebSocketHandler extends BinaryWebSocketHandler { - private final ClovaSpeechService clovaSpeechService; - private List audioDataBuffer = new ArrayList<>(); - private static final int MINIMUM_AUDIO_SIZE = 64000; // 최소 데이터 크기를 96KB로 설정 (약 3초 분량) + private final ClovaSpeechGrpcService clovaSpeechGrpcService; + private final Map> clientObservers = new ConcurrentHashMap<>(); - public SpeechWebSocketHandler(ClovaSpeechService clovaSpeechService) { - this.clovaSpeechService = clovaSpeechService; + public SpeechWebSocketHandler(ClovaSpeechGrpcService clovaSpeechGrpcService) { + this.clovaSpeechGrpcService = clovaSpeechGrpcService; } @Override - protected synchronized void handleBinaryMessage(WebSocketSession session, BinaryMessage message) throws Exception { - session.setBinaryMessageSizeLimit(256 * 1024); // 메시지 크기 제한을 256KB로 설정 - byte[] audioData = message.getPayload().array(); + public void afterConnectionEstablished(WebSocketSession session) throws Exception { + // 세션이 열릴 때마다 새로운 gRPC 스트림을 생성 + StreamObserver responseObserver = new StreamObserver() { + @Override + public void onNext(NestResponse value) { + // 서버로부터 받은 응답 처리 + try { + String contents = value.getContents(); // JSON 문자열 - // 오디오 데이터를 버퍼에 추가 - audioDataBuffer.add(audioData); + // JSON 파싱 + JsonParser parser = new JsonParser(); + JsonObject jsonObject = parser.parse(contents).getAsJsonObject(); - // 누적된 오디오 데이터 크기 계산 - int totalSize = audioDataBuffer.stream().mapToInt(arr -> arr.length).sum(); + if (jsonObject.has("transcription")) { + JsonObject transcription = jsonObject.getAsJsonObject("transcription"); + String text = transcription.get("text").getAsString(); + // 클라이언트로 text 필드만 전송 + session.sendMessage(new TextMessage(text)); + } + } catch (Exception e) { + e.printStackTrace(); + } + } - // 현재 누적된 데이터 크기를 로그로 출력 - System.out.println("현재 누적된 데이터 크기: " + totalSize + " bytes"); + @Override + public void onError(Throwable t) { + t.printStackTrace(); + try { + session.sendMessage(new TextMessage("오류 발생: " + t.getMessage())); + } catch (IOException e) { + e.printStackTrace(); + } + } - // 오디오 데이터가 충분히 쌓였을 때만 CLOVA API로 전송 - if (totalSize >= MINIMUM_AUDIO_SIZE) { - byte[] fullAudioData = mergeAudioData(); - try { - // CLOVA API로 전송 - String transcript = clovaSpeechService.recognize(fullAudioData); - session.sendMessage(new TextMessage(transcript)); - // 인식에 성공했으므로 버퍼를 초기화 - audioDataBuffer.clear(); - System.out.println("인식 성공: 버퍼를 초기화합니다."); - } catch (Exception e) { - e.printStackTrace(); - // STT007 오류 발생 시 버퍼를 유지하고 데이터 수집 계속 - if (e.getMessage().contains("STT007")) { - System.err.println("오류 발생: STT007 - 데이터가 너무 작습니다. 더 많은 데이터를 수집 중..."); - // 버퍼를 유지하여 다음 데이터를 기다립니다. - } else { - // 다른 오류 발생 시 버퍼를 초기화하고 오류 메시지 전송 - audioDataBuffer.clear(); - session.sendMessage(new TextMessage("오류 발생: " + e.getMessage())); - System.err.println("오류 발생: " + e.getMessage() + " - 버퍼를 초기화합니다."); + @Override + public void onCompleted() { + // 스트림 완료 처리 + try { + session.close(); + } catch (IOException e) { + e.printStackTrace(); } } - } else { - // 아직 데이터가 충분하지 않으면 아무 작업도 하지 않음 - System.out.println("데이터가 아직 충분하지 않음"); + }; + + // 오디오 데이터를 전송할 StreamObserver 생성 + StreamObserver requestObserver = clovaSpeechGrpcService.recognize(responseObserver); + clientObservers.put(session.getId(), requestObserver); + } + + @Override + protected void handleBinaryMessage(WebSocketSession session, BinaryMessage message) throws Exception { + // 클라이언트로부터 받은 오디오 데이터를 gRPC 서비스로 전달 + StreamObserver requestObserver = clientObservers.get(session.getId()); + if (requestObserver != null) { + byte[] audioData = message.getPayload().array(); + ByteString audioChunk = ByteString.copyFrom(audioData); + requestObserver.onNext(audioChunk); } } - // 누적된 오디오 데이터를 병합하는 메서드 - private byte[] mergeAudioData() { - int totalLength = audioDataBuffer.stream().mapToInt(arr -> arr.length).sum(); - byte[] mergedData = new byte[totalLength]; - int currentIndex = 0; - for (byte[] data : audioDataBuffer) { - System.arraycopy(data, 0, mergedData, currentIndex, data.length); - currentIndex += data.length; + @Override + public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { + // 세션이 종료되면 gRPC 스트림도 종료 + StreamObserver requestObserver = clientObservers.remove(session.getId()); + if (requestObserver != null) { + requestObserver.onCompleted(); } - return mergedData; } } diff --git a/src/main/java/com/splanet/splanet/core/properties/ClovaProperties.java b/src/main/java/com/splanet/splanet/core/properties/ClovaProperties.java index e4421cef..d223958d 100644 --- a/src/main/java/com/splanet/splanet/core/properties/ClovaProperties.java +++ b/src/main/java/com/splanet/splanet/core/properties/ClovaProperties.java @@ -8,10 +8,8 @@ @Getter @Setter @Configuration -@ConfigurationProperties(prefix = "clova") +@ConfigurationProperties(prefix = "clova.speech") public class ClovaProperties { - private String clientId; private String clientSecret; - private String url; private String language; } diff --git a/src/main/java/com/splanet/splanet/stt/controller/SpeechRecognitionController.java b/src/main/java/com/splanet/splanet/stt/controller/SpeechRecognitionController.java deleted file mode 100644 index 0c7d406a..00000000 --- a/src/main/java/com/splanet/splanet/stt/controller/SpeechRecognitionController.java +++ /dev/null @@ -1,29 +0,0 @@ -//package com.splanet.splanet.stt.controller; -// -//import com.splanet.splanet.stt.service.ClovaSpeechService; -//import org.springframework.stereotype.Controller; -//import org.springframework.web.bind.annotation.PostMapping; -//import org.springframework.web.bind.annotation.RequestMapping; -//import org.springframework.web.bind.annotation.RequestParam; -//import org.springframework.web.multipart.MultipartFile; -//import org.springframework.web.bind.annotation.RestController; -// -//@Controller -//@RequestMapping("/api/stt") -//public class SpeechRecognitionController { -// -// private final ClovaSpeechService clovaSpeechService; -// -// public SpeechRecognitionController(ClovaSpeechService clovaSpeechService) { -// this.clovaSpeechService = clovaSpeechService; -// } -// -// @PostMapping(consumes = "multipart/form-data") -// public String recognizeSpeech(@RequestParam("file") MultipartFile file) { -// try { -// return clovaSpeechService.recognize(file.getBytes()); -// } catch (Exception e) { -// throw new RuntimeException("파일 처리 중 오류 발생: " + e.getMessage()); -// } -// } -//} diff --git a/src/main/java/com/splanet/splanet/stt/service/ClovaSpeechGrpcService.java b/src/main/java/com/splanet/splanet/stt/service/ClovaSpeechGrpcService.java new file mode 100644 index 00000000..47cd0257 --- /dev/null +++ b/src/main/java/com/splanet/splanet/stt/service/ClovaSpeechGrpcService.java @@ -0,0 +1,89 @@ +package com.splanet.splanet.stt.service; + +import com.google.protobuf.ByteString; +import com.nbp.cdncp.nest.grpc.proto.v1.*; +import com.splanet.splanet.core.properties.ClovaProperties; +import io.grpc.ManagedChannel; +import io.grpc.Metadata; +import io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder; +import io.grpc.stub.MetadataUtils; +import io.grpc.stub.StreamObserver; +import org.springframework.stereotype.Service; + +@Service +public class ClovaSpeechGrpcService implements ClovaSpeechService { + + private final NestServiceGrpc.NestServiceStub nestServiceStub; + private final ClovaProperties clovaProperties; + + public ClovaSpeechGrpcService(ClovaProperties clovaProperties) { + this.clovaProperties = clovaProperties; + + // gRPC 채널 생성 + ManagedChannel channel = NettyChannelBuilder + .forAddress("clovaspeech-gw.ncloud.com", 50051) + .useTransportSecurity() + .build(); + + // Stub 생성 및 인증 정보 설정 + NestServiceGrpc.NestServiceStub stub = NestServiceGrpc.newStub(channel); + Metadata metadata = new Metadata(); + metadata.put(Metadata.Key.of("authorization", Metadata.ASCII_STRING_MARSHALLER), "Bearer " + clovaProperties.getClientSecret()); + this.nestServiceStub = MetadataUtils.attachHeaders(stub, metadata); + } + + @Override + public StreamObserver recognize(StreamObserver responseObserver) { + StreamObserver requestObserver = nestServiceStub.recognize(responseObserver); + + // Config 메시지 전송 + requestObserver.onNext(createConfigRequest(clovaProperties.getLanguage())); + + return new StreamObserver() { + private int sequenceId = 0; + + @Override + public void onNext(ByteString audioChunk) { + NestRequest dataRequest = createDataRequest(audioChunk, sequenceId, false); + requestObserver.onNext(dataRequest); + sequenceId++; + } + + @Override + public void onError(Throwable t) { + t.printStackTrace(); + requestObserver.onError(t); + } + + @Override + public void onCompleted() { + requestObserver.onCompleted(); + } + }; + } + + // Config 설정 + private NestRequest createConfigRequest(String language) { + NestConfig config = NestConfig.newBuilder() + .setConfig("{\"transcription\":{\"language\":\"" + language + "\"}}") + .build(); + + return NestRequest.newBuilder() + .setType(RequestType.CONFIG) + .setConfig(config) + .build(); + } + + // 데이터 구성 + private NestRequest createDataRequest(ByteString audioChunk, int sequenceId, boolean epFlag) { + NestData data = NestData.newBuilder() + .setChunk(audioChunk) + .setExtraContents("{\"seqId\":" + sequenceId + ",\"epFlag\":" + epFlag + "}") + .build(); + + return NestRequest.newBuilder() + .setType(RequestType.DATA) + .setData(data) + .build(); + } +} diff --git a/src/main/java/com/splanet/splanet/stt/service/ClovaSpeechService.java b/src/main/java/com/splanet/splanet/stt/service/ClovaSpeechService.java index af1551d6..ddfb9f04 100644 --- a/src/main/java/com/splanet/splanet/stt/service/ClovaSpeechService.java +++ b/src/main/java/com/splanet/splanet/stt/service/ClovaSpeechService.java @@ -1,7 +1,9 @@ package com.splanet.splanet.stt.service; -import org.springframework.web.multipart.MultipartFile; +import com.google.protobuf.ByteString; +import com.nbp.cdncp.nest.grpc.proto.v1.NestResponse; +import io.grpc.stub.StreamObserver; public interface ClovaSpeechService { - String recognize(byte[] audioBytes); + StreamObserver recognize(StreamObserver responseObserver); } diff --git a/src/main/java/com/splanet/splanet/stt/service/ClovaSpeechServiceImpl.java b/src/main/java/com/splanet/splanet/stt/service/ClovaSpeechServiceImpl.java deleted file mode 100644 index f08bf8fe..00000000 --- a/src/main/java/com/splanet/splanet/stt/service/ClovaSpeechServiceImpl.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.splanet.splanet.stt.service; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.splanet.splanet.core.exception.BusinessException; -import com.splanet.splanet.core.exception.ErrorCode; -import com.splanet.splanet.core.properties.ClovaProperties; -import org.apache.hc.client5.http.classic.methods.HttpPost; -import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; -import org.apache.hc.client5.http.impl.classic.HttpClients; -import org.apache.hc.core5.http.ContentType; -import org.apache.hc.core5.http.io.entity.ByteArrayEntity; -import org.apache.hc.core5.http.io.entity.EntityUtils; -import org.springframework.stereotype.Service; -import org.springframework.web.multipart.MultipartFile; - -@Service -public class ClovaSpeechServiceImpl implements ClovaSpeechService { - - private final ClovaProperties clovaProperties; - - public ClovaSpeechServiceImpl(ClovaProperties clovaProperties) { - this.clovaProperties = clovaProperties; - } - - @Override - public String recognize(byte[] audioBytes) { - String apiURL = clovaProperties.getUrl() + "?lang=" + clovaProperties.getLanguage(); - try (CloseableHttpClient httpClient = HttpClients.createDefault()) { - HttpPost httpPost = new HttpPost(apiURL); - httpPost.addHeader("Content-Type", "application/octet-stream"); - httpPost.addHeader("X-NCP-APIGW-API-KEY-ID", clovaProperties.getClientId()); - httpPost.addHeader("X-NCP-APIGW-API-KEY", clovaProperties.getClientSecret()); - - ByteArrayEntity byteArrayEntity = new ByteArrayEntity(audioBytes, ContentType.APPLICATION_OCTET_STREAM); - httpPost.setEntity(byteArrayEntity); - - return httpClient.execute(httpPost, response -> { - int statusCode = response.getCode(); - String responseBody = EntityUtils.toString(response.getEntity(), "UTF-8"); - - if (statusCode == 200) { - ObjectMapper objectMapper = new ObjectMapper(); - JsonNode rootNode = objectMapper.readTree(responseBody); - return rootNode.path("text").asText(); - } else { - throw new RuntimeException("CLOVA Speech API 호출 실패: " + responseBody); - } - }); - } catch (Exception e) { - throw new RuntimeException("오디오 파일 처리 중 오류 발생: " + e.getMessage()); - } - } -} \ No newline at end of file diff --git a/src/main/proto/nest.proto b/src/main/proto/nest.proto new file mode 100644 index 00000000..77e3efad --- /dev/null +++ b/src/main/proto/nest.proto @@ -0,0 +1,34 @@ +syntax = "proto3"; +option java_multiple_files = true; + +package com.nbp.cdncp.nest.grpc.proto.v1; + +enum RequestType { + CONFIG = 0; + DATA = 1; +} + +message NestConfig { + string config = 1; +} + +message NestData { + bytes chunk = 1; + string extra_contents = 2; +} + +message NestRequest { + RequestType type = 1; + oneof part { + NestConfig config = 2; + NestData data = 3; + } +} + +message NestResponse { + string contents = 1; +} + +service NestService { + rpc recognize(stream NestRequest) returns (stream NestResponse) {} +} From 87fda3d7be86b218d6cc47d2a0b34379388dc38c Mon Sep 17 00:00:00 2001 From: kanguk Date: Fri, 18 Oct 2024 19:03:10 +0900 Subject: [PATCH 40/46] =?UTF-8?q?feat:=20STT=20=EC=B4=88=EA=B8=B0=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 파일 업로드 -> STT 실행의 기본 기능을 구현한다. --- .../SpeechRecognitionController.java | 24 ++++++++ .../stt/service/ClovaSpeechServiceImpl.java | 56 +++++++++++++++++++ 2 files changed, 80 insertions(+) create mode 100644 src/main/java/com/splanet/splanet/stt/controller/SpeechRecognitionController.java create mode 100644 src/main/java/com/splanet/splanet/stt/service/ClovaSpeechServiceImpl.java diff --git a/src/main/java/com/splanet/splanet/stt/controller/SpeechRecognitionController.java b/src/main/java/com/splanet/splanet/stt/controller/SpeechRecognitionController.java new file mode 100644 index 00000000..e10f6b0c --- /dev/null +++ b/src/main/java/com/splanet/splanet/stt/controller/SpeechRecognitionController.java @@ -0,0 +1,24 @@ +package com.splanet.splanet.stt.controller; + +import com.splanet.splanet.stt.service.ClovaSpeechService; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/stt") +public class SpeechRecognitionController { + + private final ClovaSpeechService clovaSpeechService; + + public SpeechRecognitionController(ClovaSpeechService clovaSpeechService) { + this.clovaSpeechService = clovaSpeechService; + } + + @PostMapping(consumes = "multipart/form-data") + public String recognizeSpeech(@RequestParam("file") MultipartFile file) { + return clovaSpeechService.recognize(file); + } +} diff --git a/src/main/java/com/splanet/splanet/stt/service/ClovaSpeechServiceImpl.java b/src/main/java/com/splanet/splanet/stt/service/ClovaSpeechServiceImpl.java new file mode 100644 index 00000000..294d7a92 --- /dev/null +++ b/src/main/java/com/splanet/splanet/stt/service/ClovaSpeechServiceImpl.java @@ -0,0 +1,56 @@ +package com.splanet.splanet.stt.service; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.splanet.splanet.core.exception.BusinessException; +import com.splanet.splanet.core.exception.ErrorCode; +import com.splanet.splanet.core.properties.ClovaProperties; +import org.apache.hc.client5.http.classic.methods.HttpPost; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.client5.http.impl.classic.HttpClients; +import org.apache.hc.core5.http.ContentType; +import org.apache.hc.core5.http.io.entity.ByteArrayEntity; +import org.apache.hc.core5.http.io.entity.EntityUtils; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +@Service +public class ClovaSpeechServiceImpl implements ClovaSpeechService { + + private final ClovaProperties clovaProperties; + + public ClovaSpeechServiceImpl(ClovaProperties clovaProperties) { + this.clovaProperties = clovaProperties; + } + + @Override + public String recognize(MultipartFile file) { + String apiURL = clovaProperties.getUrl() + "?lang=" + clovaProperties.getLanguage(); // 언어 설정 반영 + try (CloseableHttpClient httpClient = HttpClients.createDefault()) { + byte[] audioBytes = file.getBytes(); + + HttpPost httpPost = new HttpPost(apiURL); + httpPost.addHeader("Content-Type", "application/octet-stream"); + httpPost.addHeader("X-NCP-APIGW-API-KEY-ID", clovaProperties.getClientId()); + httpPost.addHeader("X-NCP-APIGW-API-KEY", clovaProperties.getClientSecret()); + + ByteArrayEntity byteArrayEntity = new ByteArrayEntity(audioBytes, ContentType.APPLICATION_OCTET_STREAM); + httpPost.setEntity(byteArrayEntity); + + return httpClient.execute(httpPost, response -> { + int statusCode = response.getCode(); + String responseBody = EntityUtils.toString(response.getEntity(), "UTF-8"); + + if (statusCode == 200) { + ObjectMapper objectMapper = new ObjectMapper(); + JsonNode rootNode = objectMapper.readTree(responseBody); + return rootNode.path("text").asText(); + } else { + throw new BusinessException(ErrorCode.INVALID_INPUT_VALUE, "CLOVA Speech API 호출 실패: " + responseBody); + } + }); + } catch (Exception e) { + throw new BusinessException(ErrorCode.INVALID_INPUT_VALUE, "오디오 파일 처리 중 오류 발생: " + e.getMessage()); + } + } +} \ No newline at end of file From e42264adb0568184bdd09e9a202f61110dfd8a51 Mon Sep 17 00:00:00 2001 From: kanguk Date: Sat, 19 Oct 2024 02:44:20 +0900 Subject: [PATCH 41/46] =?UTF-8?q?feat:=20=EC=8B=A4=EC=8B=9C=EA=B0=84=20?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=EB=A6=AC=EB=B0=8D=20API=EB=A5=BC=20=ED=99=9C?= =?UTF-8?q?=EC=9A=A9=ED=95=98=EC=97=AC=20STT=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 기존 CLOVA Speech Recognition 에서 CLOVA Speech 실시간 스트리밍 API를 활용하여 실시간 처리를 가능하도록 수정한다. --- .../SpeechRecognitionController.java | 24 -------- .../stt/service/ClovaSpeechServiceImpl.java | 56 ------------------- 2 files changed, 80 deletions(-) delete mode 100644 src/main/java/com/splanet/splanet/stt/controller/SpeechRecognitionController.java delete mode 100644 src/main/java/com/splanet/splanet/stt/service/ClovaSpeechServiceImpl.java diff --git a/src/main/java/com/splanet/splanet/stt/controller/SpeechRecognitionController.java b/src/main/java/com/splanet/splanet/stt/controller/SpeechRecognitionController.java deleted file mode 100644 index e10f6b0c..00000000 --- a/src/main/java/com/splanet/splanet/stt/controller/SpeechRecognitionController.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.splanet.splanet.stt.controller; - -import com.splanet.splanet.stt.service.ClovaSpeechService; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.multipart.MultipartFile; -import org.springframework.web.bind.annotation.RestController; - -@RestController -@RequestMapping("/api/stt") -public class SpeechRecognitionController { - - private final ClovaSpeechService clovaSpeechService; - - public SpeechRecognitionController(ClovaSpeechService clovaSpeechService) { - this.clovaSpeechService = clovaSpeechService; - } - - @PostMapping(consumes = "multipart/form-data") - public String recognizeSpeech(@RequestParam("file") MultipartFile file) { - return clovaSpeechService.recognize(file); - } -} diff --git a/src/main/java/com/splanet/splanet/stt/service/ClovaSpeechServiceImpl.java b/src/main/java/com/splanet/splanet/stt/service/ClovaSpeechServiceImpl.java deleted file mode 100644 index 294d7a92..00000000 --- a/src/main/java/com/splanet/splanet/stt/service/ClovaSpeechServiceImpl.java +++ /dev/null @@ -1,56 +0,0 @@ -package com.splanet.splanet.stt.service; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.splanet.splanet.core.exception.BusinessException; -import com.splanet.splanet.core.exception.ErrorCode; -import com.splanet.splanet.core.properties.ClovaProperties; -import org.apache.hc.client5.http.classic.methods.HttpPost; -import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; -import org.apache.hc.client5.http.impl.classic.HttpClients; -import org.apache.hc.core5.http.ContentType; -import org.apache.hc.core5.http.io.entity.ByteArrayEntity; -import org.apache.hc.core5.http.io.entity.EntityUtils; -import org.springframework.stereotype.Service; -import org.springframework.web.multipart.MultipartFile; - -@Service -public class ClovaSpeechServiceImpl implements ClovaSpeechService { - - private final ClovaProperties clovaProperties; - - public ClovaSpeechServiceImpl(ClovaProperties clovaProperties) { - this.clovaProperties = clovaProperties; - } - - @Override - public String recognize(MultipartFile file) { - String apiURL = clovaProperties.getUrl() + "?lang=" + clovaProperties.getLanguage(); // 언어 설정 반영 - try (CloseableHttpClient httpClient = HttpClients.createDefault()) { - byte[] audioBytes = file.getBytes(); - - HttpPost httpPost = new HttpPost(apiURL); - httpPost.addHeader("Content-Type", "application/octet-stream"); - httpPost.addHeader("X-NCP-APIGW-API-KEY-ID", clovaProperties.getClientId()); - httpPost.addHeader("X-NCP-APIGW-API-KEY", clovaProperties.getClientSecret()); - - ByteArrayEntity byteArrayEntity = new ByteArrayEntity(audioBytes, ContentType.APPLICATION_OCTET_STREAM); - httpPost.setEntity(byteArrayEntity); - - return httpClient.execute(httpPost, response -> { - int statusCode = response.getCode(); - String responseBody = EntityUtils.toString(response.getEntity(), "UTF-8"); - - if (statusCode == 200) { - ObjectMapper objectMapper = new ObjectMapper(); - JsonNode rootNode = objectMapper.readTree(responseBody); - return rootNode.path("text").asText(); - } else { - throw new BusinessException(ErrorCode.INVALID_INPUT_VALUE, "CLOVA Speech API 호출 실패: " + responseBody); - } - }); - } catch (Exception e) { - throw new BusinessException(ErrorCode.INVALID_INPUT_VALUE, "오디오 파일 처리 중 오류 발생: " + e.getMessage()); - } - } -} \ No newline at end of file From 23362373539ef09fd39eac4e285673681bfc0002 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=84=B8=EC=A7=84?= Date: Tue, 22 Oct 2024 23:03:43 +0900 Subject: [PATCH 42/46] =?UTF-8?q?feat:=20SpringAi=20gpt=20=EC=9A=B0?= =?UTF-8?q?=EC=84=A0=20=EC=97=B0=EA=B2=B0=EB=A7=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 14 ++++++ .../splanet/splanet/config/OpenAIConfig.java | 21 +++++++++ .../com/splanet/splanet/gpt/ChatResponse.java | 9 ++++ .../java/com/splanet/splanet/gpt/Choice.java | 8 ++++ .../java/com/splanet/splanet/gpt/Message.java | 12 +++++ .../splanet/splanet/gpt/OpenAiChatClient.java | 47 +++++++++++++++++++ .../java/com/splanet/splanet/gpt/Prompt.java | 12 +++++ 7 files changed, 123 insertions(+) create mode 100644 src/main/java/com/splanet/splanet/config/OpenAIConfig.java create mode 100644 src/main/java/com/splanet/splanet/gpt/ChatResponse.java create mode 100644 src/main/java/com/splanet/splanet/gpt/Choice.java create mode 100644 src/main/java/com/splanet/splanet/gpt/Message.java create mode 100644 src/main/java/com/splanet/splanet/gpt/OpenAiChatClient.java create mode 100644 src/main/java/com/splanet/splanet/gpt/Prompt.java diff --git a/build.gradle b/build.gradle index f02fed08..bde34fb4 100644 --- a/build.gradle +++ b/build.gradle @@ -23,6 +23,12 @@ configurations { repositories { mavenCentral() + maven { + url 'https://repo.spring.io/milestone' + } + maven { + url 'https://repo.spring.io/snapshot' + } } dependencies { @@ -61,6 +67,14 @@ dependencies { testImplementation 'org.springframework.security:spring-security-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + //gpt + // implementation 'org.springframework.boot:spring-boot-starter-web' + // implementation 'com.fasterxml.jackson.core:jackson-databind' + // implementation 'org.springframework.boot:spring-boot-starter' + implementation platform("org.springframework.ai:spring-ai-bom:0.8.0") + implementation 'org.springframework.ai:spring-ai-openai' + + } tasks.named('test') { diff --git a/src/main/java/com/splanet/splanet/config/OpenAIConfig.java b/src/main/java/com/splanet/splanet/config/OpenAIConfig.java new file mode 100644 index 00000000..974a3739 --- /dev/null +++ b/src/main/java/com/splanet/splanet/config/OpenAIConfig.java @@ -0,0 +1,21 @@ +package com.splanet.splanet.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +@Configuration +public class OpenAIConfig { + @Value("${spring.ai.gpt.api-key}") + private String apiKey; + @Bean + public RestTemplate template(){ + RestTemplate restTemplate = new RestTemplate(); + restTemplate.getInterceptors().add((request, body, execution) -> { + request.getHeaders().add("Authorization", "Bearer " + apiKey); + return execution.execute(request, body); + }); + return restTemplate; + } +} \ No newline at end of file diff --git a/src/main/java/com/splanet/splanet/gpt/ChatResponse.java b/src/main/java/com/splanet/splanet/gpt/ChatResponse.java new file mode 100644 index 00000000..25d92c71 --- /dev/null +++ b/src/main/java/com/splanet/splanet/gpt/ChatResponse.java @@ -0,0 +1,9 @@ +package com.splanet.splanet.gpt; + +import lombok.Getter; +import java.util.List; + +@Getter +public class ChatResponse { + private List choices; // Choice 객체 리스트 +} \ No newline at end of file diff --git a/src/main/java/com/splanet/splanet/gpt/Choice.java b/src/main/java/com/splanet/splanet/gpt/Choice.java new file mode 100644 index 00000000..be2070a0 --- /dev/null +++ b/src/main/java/com/splanet/splanet/gpt/Choice.java @@ -0,0 +1,8 @@ +package com.splanet.splanet.gpt; + +import lombok.Getter; + +@Getter +public class Choice { + private Message message; // Message 객체 +} \ No newline at end of file diff --git a/src/main/java/com/splanet/splanet/gpt/Message.java b/src/main/java/com/splanet/splanet/gpt/Message.java new file mode 100644 index 00000000..55b59650 --- /dev/null +++ b/src/main/java/com/splanet/splanet/gpt/Message.java @@ -0,0 +1,12 @@ +package com.splanet.splanet.gpt; + +import lombok.Getter; + +@Getter +public class Message { + private String content; // 메시지 내용 + + public Message(String content) { + this.content = content; + } +} \ No newline at end of file diff --git a/src/main/java/com/splanet/splanet/gpt/OpenAiChatClient.java b/src/main/java/com/splanet/splanet/gpt/OpenAiChatClient.java new file mode 100644 index 00000000..27da861e --- /dev/null +++ b/src/main/java/com/splanet/splanet/gpt/OpenAiChatClient.java @@ -0,0 +1,47 @@ +package com.splanet.splanet.gpt; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Flux; + +import java.util.List; + +@Component +public class OpenAiChatClient { + + private final WebClient webClient; + + @Value("${spring.ai.gpt.api-key}") + private String apiKey; + + public OpenAiChatClient(WebClient.Builder webClientBuilder) { + this.webClient = webClientBuilder.baseUrl("https://api.openai.com/v1").build(); + } + + public String call(String message) { + Message userMessage = new Message(message); // 메시지 객체 생성 + Prompt prompt = new Prompt(List.of(userMessage)); // Message 객체를 리스트로 감싸서 Prompt 생성 + + ChatResponse response = webClient.post() + .uri("/chat/completions") + .header("Authorization", "Bearer " + apiKey) // 프로퍼티에서 가져온 API 키 사용 + .bodyValue(prompt) + .retrieve() + .bodyToMono(ChatResponse.class) + .block(); // 블록하여 응답을 기다림 + + return response != null && response.getChoices() != null && !response.getChoices().isEmpty() + ? response.getChoices().get(0).getMessage().getContent() + : "No response"; + } + + public Flux stream(Prompt prompt) { + return webClient.post() + .uri("/chat/completions") + .header("Authorization", "Bearer " + apiKey) // 프로퍼티에서 가져온 API 키 사용 + .bodyValue(prompt) + .retrieve() + .bodyToFlux(ChatResponse.class); // Stream the response + } +} \ No newline at end of file diff --git a/src/main/java/com/splanet/splanet/gpt/Prompt.java b/src/main/java/com/splanet/splanet/gpt/Prompt.java new file mode 100644 index 00000000..27da96c1 --- /dev/null +++ b/src/main/java/com/splanet/splanet/gpt/Prompt.java @@ -0,0 +1,12 @@ +package com.splanet.splanet.gpt; + +import lombok.Getter; +import lombok.AllArgsConstructor; + +import java.util.List; + +@Getter +@AllArgsConstructor // 모든 필드를 사용하는 생성자를 자동 생성 +public class Prompt { + private List messages; // 메시지 목록 +} \ No newline at end of file From 87fc3482111c4a812389bdf16ec45935e02f52fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=84=B8=EC=A7=84?= Date: Wed, 23 Oct 2024 20:43:03 +0900 Subject: [PATCH 43/46] refactor: @Value->@Configurationproperties --- .../splanet/splanet/config/OpenAIConfig.java | 21 ------------------- .../splanet/splanet/gpt/OpenAiChatClient.java | 20 ++++++++---------- .../splanet/splanet/gpt/OpenAiProperties.java | 14 +++++++++++++ 3 files changed, 23 insertions(+), 32 deletions(-) delete mode 100644 src/main/java/com/splanet/splanet/config/OpenAIConfig.java create mode 100644 src/main/java/com/splanet/splanet/gpt/OpenAiProperties.java diff --git a/src/main/java/com/splanet/splanet/config/OpenAIConfig.java b/src/main/java/com/splanet/splanet/config/OpenAIConfig.java deleted file mode 100644 index 974a3739..00000000 --- a/src/main/java/com/splanet/splanet/config/OpenAIConfig.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.splanet.splanet.config; - -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.web.client.RestTemplate; - -@Configuration -public class OpenAIConfig { - @Value("${spring.ai.gpt.api-key}") - private String apiKey; - @Bean - public RestTemplate template(){ - RestTemplate restTemplate = new RestTemplate(); - restTemplate.getInterceptors().add((request, body, execution) -> { - request.getHeaders().add("Authorization", "Bearer " + apiKey); - return execution.execute(request, body); - }); - return restTemplate; - } -} \ No newline at end of file diff --git a/src/main/java/com/splanet/splanet/gpt/OpenAiChatClient.java b/src/main/java/com/splanet/splanet/gpt/OpenAiChatClient.java index 27da861e..363e30d7 100644 --- a/src/main/java/com/splanet/splanet/gpt/OpenAiChatClient.java +++ b/src/main/java/com/splanet/splanet/gpt/OpenAiChatClient.java @@ -1,6 +1,5 @@ package com.splanet.splanet.gpt; -import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import org.springframework.web.reactive.function.client.WebClient; import reactor.core.publisher.Flux; @@ -11,25 +10,24 @@ public class OpenAiChatClient { private final WebClient webClient; + private final OpenAiProperties openAiProperties; - @Value("${spring.ai.gpt.api-key}") - private String apiKey; - - public OpenAiChatClient(WebClient.Builder webClientBuilder) { + public OpenAiChatClient(WebClient.Builder webClientBuilder, OpenAiProperties openAiProperties) { this.webClient = webClientBuilder.baseUrl("https://api.openai.com/v1").build(); + this.openAiProperties = openAiProperties; } public String call(String message) { - Message userMessage = new Message(message); // 메시지 객체 생성 - Prompt prompt = new Prompt(List.of(userMessage)); // Message 객체를 리스트로 감싸서 Prompt 생성 + Message userMessage = new Message(message); + Prompt prompt = new Prompt(List.of(userMessage)); ChatResponse response = webClient.post() .uri("/chat/completions") - .header("Authorization", "Bearer " + apiKey) // 프로퍼티에서 가져온 API 키 사용 + .header("Authorization", "Bearer " + openAiProperties.getApiKey()) .bodyValue(prompt) .retrieve() .bodyToMono(ChatResponse.class) - .block(); // 블록하여 응답을 기다림 + .block(); return response != null && response.getChoices() != null && !response.getChoices().isEmpty() ? response.getChoices().get(0).getMessage().getContent() @@ -39,9 +37,9 @@ public String call(String message) { public Flux stream(Prompt prompt) { return webClient.post() .uri("/chat/completions") - .header("Authorization", "Bearer " + apiKey) // 프로퍼티에서 가져온 API 키 사용 + .header("Authorization", "Bearer " + openAiProperties.getApiKey()) .bodyValue(prompt) .retrieve() - .bodyToFlux(ChatResponse.class); // Stream the response + .bodyToFlux(ChatResponse.class); } } \ No newline at end of file diff --git a/src/main/java/com/splanet/splanet/gpt/OpenAiProperties.java b/src/main/java/com/splanet/splanet/gpt/OpenAiProperties.java new file mode 100644 index 00000000..282fa863 --- /dev/null +++ b/src/main/java/com/splanet/splanet/gpt/OpenAiProperties.java @@ -0,0 +1,14 @@ +package com.splanet.splanet.gpt; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Getter +@Setter +@ConfigurationProperties(prefix = "gpt-api-key") +@Configuration +public class OpenAiProperties { + private String apiKey; +} \ No newline at end of file From 7b28ec4428fb9835734c6c930ac9d15be224896a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=84=B8=EC=A7=84?= Date: Fri, 25 Oct 2024 01:14:17 +0900 Subject: [PATCH 44/46] =?UTF-8?q?feat:=20=EC=8A=A4=EC=BC=80=EC=A4=84=20?= =?UTF-8?q?=EC=9D=91=EB=8B=B5=20=ED=94=84=EB=A1=AC=ED=94=84=ED=8A=B8=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/splanet/splanet/gpt/ChatResponse.java | 9 ----- .../java/com/splanet/splanet/gpt/Choice.java | 8 ---- .../java/com/splanet/splanet/gpt/Message.java | 9 ++--- .../splanet/splanet/gpt/OpenAiChatClient.java | 36 ++++++++++++------ .../com/splanet/splanet/gpt/RequestBody.java | 13 +++++++ .../splanet/gpt/SchedulePromptGenerator.java | 38 +++++++++++++++++++ .../splanet/splanet/gpt/ScheduleRequest.java | 19 ++++++++++ .../splanet/splanet/gpt/ScheduleResponse.java | 33 ++++++++++++++++ 8 files changed, 131 insertions(+), 34 deletions(-) delete mode 100644 src/main/java/com/splanet/splanet/gpt/ChatResponse.java delete mode 100644 src/main/java/com/splanet/splanet/gpt/Choice.java create mode 100644 src/main/java/com/splanet/splanet/gpt/RequestBody.java create mode 100644 src/main/java/com/splanet/splanet/gpt/SchedulePromptGenerator.java create mode 100644 src/main/java/com/splanet/splanet/gpt/ScheduleRequest.java create mode 100644 src/main/java/com/splanet/splanet/gpt/ScheduleResponse.java diff --git a/src/main/java/com/splanet/splanet/gpt/ChatResponse.java b/src/main/java/com/splanet/splanet/gpt/ChatResponse.java deleted file mode 100644 index 25d92c71..00000000 --- a/src/main/java/com/splanet/splanet/gpt/ChatResponse.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.splanet.splanet.gpt; - -import lombok.Getter; -import java.util.List; - -@Getter -public class ChatResponse { - private List choices; // Choice 객체 리스트 -} \ No newline at end of file diff --git a/src/main/java/com/splanet/splanet/gpt/Choice.java b/src/main/java/com/splanet/splanet/gpt/Choice.java deleted file mode 100644 index be2070a0..00000000 --- a/src/main/java/com/splanet/splanet/gpt/Choice.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.splanet.splanet.gpt; - -import lombok.Getter; - -@Getter -public class Choice { - private Message message; // Message 객체 -} \ No newline at end of file diff --git a/src/main/java/com/splanet/splanet/gpt/Message.java b/src/main/java/com/splanet/splanet/gpt/Message.java index 55b59650..c64ff08c 100644 --- a/src/main/java/com/splanet/splanet/gpt/Message.java +++ b/src/main/java/com/splanet/splanet/gpt/Message.java @@ -1,12 +1,11 @@ package com.splanet.splanet.gpt; +import lombok.AllArgsConstructor; import lombok.Getter; @Getter -public class Message { +@AllArgsConstructor +class Message { + private String role; // 메시지의 역할 (user, assistant 등) private String content; // 메시지 내용 - - public Message(String content) { - this.content = content; - } } \ No newline at end of file diff --git a/src/main/java/com/splanet/splanet/gpt/OpenAiChatClient.java b/src/main/java/com/splanet/splanet/gpt/OpenAiChatClient.java index 363e30d7..e1249a10 100644 --- a/src/main/java/com/splanet/splanet/gpt/OpenAiChatClient.java +++ b/src/main/java/com/splanet/splanet/gpt/OpenAiChatClient.java @@ -1,5 +1,7 @@ package com.splanet.splanet.gpt; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.stereotype.Component; import org.springframework.web.reactive.function.client.WebClient; import reactor.core.publisher.Flux; @@ -11,35 +13,45 @@ public class OpenAiChatClient { private final WebClient webClient; private final OpenAiProperties openAiProperties; + private final SchedulePromptGenerator promptGenerator; - public OpenAiChatClient(WebClient.Builder webClientBuilder, OpenAiProperties openAiProperties) { + public OpenAiChatClient(WebClient.Builder webClientBuilder, OpenAiProperties openAiProperties, SchedulePromptGenerator promptGenerator) { this.webClient = webClientBuilder.baseUrl("https://api.openai.com/v1").build(); this.openAiProperties = openAiProperties; + this.promptGenerator = new SchedulePromptGenerator(); } - public String call(String message) { - Message userMessage = new Message(message); - Prompt prompt = new Prompt(List.of(userMessage)); + // 스케줄 생성 요청 처리 메소드 + public ScheduleResponse createSchedule(ScheduleRequest scheduleRequest) throws JsonProcessingException { + String jsonResponse = call(scheduleRequest); + ObjectMapper objectMapper = new ObjectMapper(); + return objectMapper.readValue(jsonResponse, ScheduleResponse.class); + } + + private String call(ScheduleRequest scheduleRequest) throws JsonProcessingException { + String jsonRequest = new ObjectMapper().writeValueAsString(new RequestBody("gpt-4o-mini", List.of( + new Message("user", promptGenerator.generateSchedulePrompt(scheduleRequest)) + ))); - ChatResponse response = webClient.post() + // OpenAI API 호출 + String responseJson = webClient.post() .uri("/chat/completions") .header("Authorization", "Bearer " + openAiProperties.getApiKey()) - .bodyValue(prompt) + .bodyValue(jsonRequest) .retrieve() - .bodyToMono(ChatResponse.class) + .bodyToMono(String.class) .block(); - return response != null && response.getChoices() != null && !response.getChoices().isEmpty() - ? response.getChoices().get(0).getMessage().getContent() - : "No response"; + return responseJson; } - public Flux stream(Prompt prompt) { + // 스트리밍 메소드 + public Flux stream(Prompt prompt) { return webClient.post() .uri("/chat/completions") .header("Authorization", "Bearer " + openAiProperties.getApiKey()) .bodyValue(prompt) .retrieve() - .bodyToFlux(ChatResponse.class); + .bodyToFlux(ScheduleResponse.class); } } \ No newline at end of file diff --git a/src/main/java/com/splanet/splanet/gpt/RequestBody.java b/src/main/java/com/splanet/splanet/gpt/RequestBody.java new file mode 100644 index 00000000..35983b26 --- /dev/null +++ b/src/main/java/com/splanet/splanet/gpt/RequestBody.java @@ -0,0 +1,13 @@ +package com.splanet.splanet.gpt; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.List; + +@Getter +@AllArgsConstructor +public class RequestBody { + private String model; // 모델 이름 + private List messages; // 메시지 리스트 +} \ No newline at end of file diff --git a/src/main/java/com/splanet/splanet/gpt/SchedulePromptGenerator.java b/src/main/java/com/splanet/splanet/gpt/SchedulePromptGenerator.java new file mode 100644 index 00000000..40e09d7e --- /dev/null +++ b/src/main/java/com/splanet/splanet/gpt/SchedulePromptGenerator.java @@ -0,0 +1,38 @@ +package com.splanet.splanet.gpt; + +import org.springframework.stereotype.Component; + +@Component +public class SchedulePromptGenerator { + + public String generateSchedulePrompt(ScheduleRequest request) { + StringBuilder prompt = new StringBuilder(); + + prompt.append("splanet은 사용자가 입력한 스케줄 정보에 맞춰 맞춤형 플래너를 제공하는 서비스입니다. 사용자가 음성으로 입력한 정보를 분석하여 최적의 스케줄을 제시해야 합니다.\n\n") + .append("사용자가 다음과 같은 정보를 입력했습니다. 이 정보를 바탕으로 3개의 서로 다른 스케줄을 추천해 주세요. 각 스케줄은 고유한 컨셉을 가져야 하며, 하루 24시간을 30분 단위로 쪼개고, 각 업무의 시작 시간과 종료 시간을 포함해야 합니다.\n\n") + .append("요청 정보:\n") + .append("- 스케줄 기간: \"").append(request.getSchedulePeriod()).append("\"\n") + .append("- 업무 목록: ").append(request.getTaskList()).append("\n") + .append("- 업무 소요 시간: ").append(request.getTaskDurations()).append("\n") + .append("- 우선순위: ").append(request.getPriority()).append("\n") + .append("- 스케줄 컨셉: \"").append(request.getScheduleConcepts().get(0)).append("\", \"") + .append(request.getScheduleConcepts().get(1)).append("\", \"") + .append(request.getScheduleConcepts().get(2)).append("\"\n") // 각 컨셉을 따로 넣도록 + .append("- 하루를 30분 단위로 쪼갠 시간 목록: ").append(request.getTimeSlots()).append("\n") + .append("위 정보를 바탕으로 최적의 플래너 스케줄을 추천할 때 다음 사항을 준수해주세요:\n") + .append("1. 요청된 모든 일정을 사용해야 합니다.\n") + .append("2. 각 스케줄은 고유한 컨셉을 가져야 하며, 3개의 서로 다른 스케줄을 추천합니다.\n") + .append("3. 추천 예시: 사용자가 선택할 수 있도록 3개의 추천 스케줄을 제공합니다.\n") + .append("4. 응답 형식: 답변은 JSON 형식으로만 제공해야 하며, 다음과 같은 구조를 따라야 합니다:\n") + .append(" { \"schedules\": [{ \"concept\": \"스케줄 컨셉\", \"schedule\": [{ \"date\": \"MM-DD\", \"tasks\": [{ \"task\": \"업무명\", \"duration\": \"소요시간\", \"priority\": \"우선순위\", \"startTime\": \"시작시간\", \"endTime\": \"종료시간\" }] }] }] }] }\n") + .append("5. 형식 규칙:\n") + .append(" - 날짜는 MM-DD 형식으로, 시간은 24시간(30분 단위) 형식이어야 합니다.\n") + .append(" - 입력받은 업무를 지정된 일정 안에 모두 포함시켜야 합니다.\n") + .append(" - 우선순위는 고유한 정수 값이어야 합니다.\n") + .append(" - 업무 시간은 주어진 스케줄 기간 안에만 분할하여 채울 수 있습니다.\n") + .append(" - 입력받은 날짜 각각의 일정을 구현해야 합니다.\n") + .append("6. 제외할 내용: 일정 JSON 외에는 다른 내용이 포함되지 않아야 하며, 스케줄링과 관련 없는 질문에는 \"이와 관련된 질문에는 답변할 수 없습니다.\"라고 응답해야 합니다.\n"); + + return prompt.toString(); + } +} \ No newline at end of file diff --git a/src/main/java/com/splanet/splanet/gpt/ScheduleRequest.java b/src/main/java/com/splanet/splanet/gpt/ScheduleRequest.java new file mode 100644 index 00000000..e6f254e0 --- /dev/null +++ b/src/main/java/com/splanet/splanet/gpt/ScheduleRequest.java @@ -0,0 +1,19 @@ +package com.splanet.splanet.gpt; + +import lombok.*; + +import java.util.List; +import java.util.Map; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class ScheduleRequest { + private String schedulePeriod; // 스케줄 기간 + private List taskList; // 업무 목록 + private Map taskDurations; // 업무 소요 시간 (업무명: 소요시간) + private Map priority; // 업무 우선순위 (업무명: 우선순위) + private List scheduleConcepts; // 스케줄 컨셉 목록 + private List timeSlots; // 30분 단위 시간 목록 +} diff --git a/src/main/java/com/splanet/splanet/gpt/ScheduleResponse.java b/src/main/java/com/splanet/splanet/gpt/ScheduleResponse.java new file mode 100644 index 00000000..86b4dada --- /dev/null +++ b/src/main/java/com/splanet/splanet/gpt/ScheduleResponse.java @@ -0,0 +1,33 @@ +package com.splanet.splanet.gpt; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Getter +@AllArgsConstructor +@NoArgsConstructor +public class ScheduleResponse { + private List schedules; // 스케줄 리스트 + + @Getter + @AllArgsConstructor + @NoArgsConstructor + public static class Schedule { + private String concept; // 스케줄 컨셉 + private List tasks; // 업무 리스트 + + @Getter + @AllArgsConstructor + @NoArgsConstructor + public static class Task { + private String task; // 업무명 + private String duration; // 소요 시간 + private int priority; // 우선순위 + private String startTime; // 시작 시간 + private String endTime; // 종료 시간 + } + } +} \ No newline at end of file From bbec73b1e28acb254df54f7538f219a0da2c5823 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=84=B8=EC=A7=84?= Date: Fri, 25 Oct 2024 01:46:42 +0900 Subject: [PATCH 45/46] =?UTF-8?q?refactor:=20=EC=97=AC=EB=9F=AC=20?= =?UTF-8?q?=EC=8A=A4=EC=BC=80=EC=A4=84=20=EB=B0=9B=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../splanet/gpt/SchedulePromptGenerator.java | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/splanet/splanet/gpt/SchedulePromptGenerator.java b/src/main/java/com/splanet/splanet/gpt/SchedulePromptGenerator.java index 40e09d7e..f3266881 100644 --- a/src/main/java/com/splanet/splanet/gpt/SchedulePromptGenerator.java +++ b/src/main/java/com/splanet/splanet/gpt/SchedulePromptGenerator.java @@ -8,21 +8,19 @@ public class SchedulePromptGenerator { public String generateSchedulePrompt(ScheduleRequest request) { StringBuilder prompt = new StringBuilder(); - prompt.append("splanet은 사용자가 입력한 스케줄 정보에 맞춰 맞춤형 플래너를 제공하는 서비스입니다. 사용자가 음성으로 입력한 정보를 분석하여 최적의 스케줄을 제시해야 합니다.\n\n") - .append("사용자가 다음과 같은 정보를 입력했습니다. 이 정보를 바탕으로 3개의 서로 다른 스케줄을 추천해 주세요. 각 스케줄은 고유한 컨셉을 가져야 하며, 하루 24시간을 30분 단위로 쪼개고, 각 업무의 시작 시간과 종료 시간을 포함해야 합니다.\n\n") + prompt.append("splanet은 사용자가 입력한 스케줄 정보를 바탕으로 맞춤형 플래너를 제공하는 서비스입니다. 사용자가 음성으로 입력한 정보를 분석하여 최적의 스케줄을 제시해야 합니다.\n\n") + .append("사용자가 다음과 같은 정보를 입력했습니다. 이 정보를 바탕으로 요청된 컨셉에 따라 스케줄을 추천해 주세요. 각 스케줄은 하루 24시간을 30분 단위로 쪼개고, 각 업무의 시작 시간과 종료 시간을 포함해야 합니다.\n\n") .append("요청 정보:\n") - .append("- 스케줄 기간: \"").append(request.getSchedulePeriod()).append("\"\n") + .append("- 스케줄 기간: \"10월 1일부터 10월 2일까지\"\n") .append("- 업무 목록: ").append(request.getTaskList()).append("\n") .append("- 업무 소요 시간: ").append(request.getTaskDurations()).append("\n") .append("- 우선순위: ").append(request.getPriority()).append("\n") - .append("- 스케줄 컨셉: \"").append(request.getScheduleConcepts().get(0)).append("\", \"") - .append(request.getScheduleConcepts().get(1)).append("\", \"") - .append(request.getScheduleConcepts().get(2)).append("\"\n") // 각 컨셉을 따로 넣도록 + .append("- 스케줄 컨셉: \"널널한 스케줄\", \"빡빡한 스케줄\"\n") .append("- 하루를 30분 단위로 쪼갠 시간 목록: ").append(request.getTimeSlots()).append("\n") - .append("위 정보를 바탕으로 최적의 플래너 스케줄을 추천할 때 다음 사항을 준수해주세요:\n") + .append("위 정보를 바탕으로 다음과 같은 조건을 준수하여 스케줄을 추천해주세요:\n") .append("1. 요청된 모든 일정을 사용해야 합니다.\n") - .append("2. 각 스케줄은 고유한 컨셉을 가져야 하며, 3개의 서로 다른 스케줄을 추천합니다.\n") - .append("3. 추천 예시: 사용자가 선택할 수 있도록 3개의 추천 스케줄을 제공합니다.\n") + .append("2. 각 스케줄은 요청된 두 개의 서로 다른 컨셉(사용자가 입력한다. 예시로는 널널한 스케줄, 빡빡한 스케줄이 있다.)을 따릅니다.\n") + .append("3. 추천 예시: 사용자가 선택할 수 있도록 두 개의 추천 스케줄을 제공합니다.\n") .append("4. 응답 형식: 답변은 JSON 형식으로만 제공해야 하며, 다음과 같은 구조를 따라야 합니다:\n") .append(" { \"schedules\": [{ \"concept\": \"스케줄 컨셉\", \"schedule\": [{ \"date\": \"MM-DD\", \"tasks\": [{ \"task\": \"업무명\", \"duration\": \"소요시간\", \"priority\": \"우선순위\", \"startTime\": \"시작시간\", \"endTime\": \"종료시간\" }] }] }] }] }\n") .append("5. 형식 규칙:\n") @@ -31,7 +29,8 @@ public String generateSchedulePrompt(ScheduleRequest request) { .append(" - 우선순위는 고유한 정수 값이어야 합니다.\n") .append(" - 업무 시간은 주어진 스케줄 기간 안에만 분할하여 채울 수 있습니다.\n") .append(" - 입력받은 날짜 각각의 일정을 구현해야 합니다.\n") - .append("6. 제외할 내용: 일정 JSON 외에는 다른 내용이 포함되지 않아야 하며, 스케줄링과 관련 없는 질문에는 \"이와 관련된 질문에는 답변할 수 없습니다.\"라고 응답해야 합니다.\n"); + .append("6. 예시: 결과값으로 각 날짜마다 널널한 스케줄과 빡빡한 스케줄 컨셉을 입력받으면, 스케줄1(빡빡한 스케줄): 10월1일+10월2일, 스케줄2(널널한 스케줄): 10월1일+10월2일 총 4개의 스케줄을 제시해야 합니다. 오직 스케줄 데이터 정보만 json으로 출력한다.\n") + .append("7. 제외할 내용: 일정 JSON 외에는 다른 내용이 포함되지 않아야 하며, 스케줄링과 관련 없는 질문에는 \"이와 관련된 질문에는 답변할 수 없습니다.\"라고 응답해야 합니다.\n"); return prompt.toString(); } From 2ab1f47eb1594c06eaeb5ec3e06246b1762364df Mon Sep 17 00:00:00 2001 From: kanguk Date: Fri, 25 Oct 2024 14:25:00 +0900 Subject: [PATCH 46/46] =?UTF-8?q?fix:=20ci=20=ED=8A=B8=EB=A6=AC=EA=B1=B0?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pr 올릴 시 ci워크플로우가 두번 중복되어 실행되는 문제가 있어서 트리거를 수정한다. --- .github/workflows/ci.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cebd9573..4948fb66 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,11 +1,6 @@ name: CI (빌드 및 테스트) on: - push: - branches: - - master - - develop - - 'weekly/**' pull_request: branches: - master