Skip to content

Commit

Permalink
MATE-155 : [FEAT] 굿즈 거래완료 메시지 전송 기능 구현 (#139)
Browse files Browse the repository at this point in the history
* MATE-155 : [REFACTOR] 굿즈거래 거래완료 기능 GoodsChatRoom 도메인으로 변경

* MATE-155 : [FEAT] 굿즈 거래완료 메시지 전송 기능 구현

* MATE-155 : [TEST] 도메인 변경으로 인한 테스트 코드 삭제

* MATE-155 : [TEST] 굿즈 거래완료 테스트 코드 작성
  • Loading branch information
hongjeZZ authored Jan 16, 2025
1 parent 14a9a60 commit b73e4e6
Show file tree
Hide file tree
Showing 13 changed files with 205 additions and 239 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
public enum MessageType {
ENTER("입장"), // 채팅방 입장
TALK("대화"), // 일반 메시지
LEAVE("퇴장"); // 채팅방 퇴장
LEAVE("퇴장"), // 채팅방 퇴장
GOODS("굿즈"); // 굿즈 거래완료

private final String value;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public class GoodsChatRoomController {
private final GoodsChatService goodsChatService;

@PostMapping
@Operation(summary = "굿즈거래 채팅방 입장 및 생성", description = "굿즈 거래 게시글에 대한 채팅방을 생성하거나 기존 채팅방 정보를 조회합니다.")
@Operation(summary = "굿즈거래 채팅방 입장 및 생성", description = "굿즈거래 게시글에 대한 채팅방을 생성하거나 기존 채팅방 정보를 조회합니다.")
public ResponseEntity<ApiResponse<GoodsChatRoomResponse>> createGoodsChatRoom(
@AuthenticationPrincipal AuthMember member,
@Parameter(description = "판매글 ID", required = true) @RequestParam Long goodsPostId
Expand Down Expand Up @@ -75,7 +75,7 @@ public ResponseEntity<Void> leaveGoodsChatRoom(
}

@GetMapping("/{chatRoomId}")
@Operation(summary = "굿즈거래 채팅방 입장", description = "굿즈 거래 채팅방의 정보를 조회합니다.")
@Operation(summary = "굿즈거래 채팅방 입장", description = "굿즈거래 채팅방의 정보를 조회합니다.")
public ResponseEntity<ApiResponse<GoodsChatRoomResponse>> getGoodsChatRoomInfo(
@AuthenticationPrincipal AuthMember member,
@Parameter(description = "채팅방 ID", required = true) @PathVariable Long chatRoomId
Expand All @@ -93,4 +93,14 @@ public ResponseEntity<ApiResponse<List<MemberSummaryResponse>>> getGoodsChatRoom
List<MemberSummaryResponse> responses = goodsChatService.getMembersInChatRoom(member.getMemberId(), chatRoomId);
return ResponseEntity.ok(ApiResponse.success(responses));
}

@PostMapping("/{chatRoomId}/complete")
@Operation(summary = "굿즈 거래 완료", description = "굿즈거래 채팅방에서 굿즈거래를 거래완료 처리합니다.")
public ResponseEntity<ApiResponse<Void>> completeGoodsPost(
@AuthenticationPrincipal AuthMember member,
@Parameter(description = "채팅방 ID", required = true) @PathVariable Long chatRoomId
) {
goodsChatService.completeTransaction(member.getMemberId(), chatRoomId);
return ResponseEntity.ok(ApiResponse.success(null));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@
import lombok.RequiredArgsConstructor;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.event.TransactionalEventListener;

@Component
Expand All @@ -16,7 +14,6 @@ public class GoodsChatEventHandler {

@Async
@TransactionalEventListener
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void handle(GoodsChatEvent event) {
messageService.sendChatEventMessage(event);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,11 @@ public class GoodsChatMessageService {
private final GoodsChatMessageRepository messageRepository;
private final SimpMessagingTemplate messagingTemplate;

private static final String GOODS_CHAT_SUBSCRIBE_PATH = "/sub/chat/goods/";

private static final String MEMBER_ENTER_MESSAGE = "님이 대화를 시작했습니다.";
private static final String MEMBER_LEAVE_MESSAGE = "님이 대화를 떠났습니다.";
private static final String MEMBER_TRANSACTION_MESSAGE = "님이 거래를 완료했습니다. 상품에 대한 거래후기를 남겨주세요!";

public void sendMessage(GoodsChatMessageRequest message) {
Member member = findMemberById(message.getSenderId());
Expand All @@ -44,7 +47,7 @@ public void sendMessage(GoodsChatMessageRequest message) {
sendToSubscribers(message.getRoomId(), response);
}

// 입장 및 퇴장 메시지 전송
// 이벤트 메시지 전송
public void sendChatEventMessage(GoodsChatEvent event) {
Member member = event.member();
Long chatRoomId = event.chatRoomId();
Expand All @@ -55,6 +58,7 @@ public void sendChatEventMessage(GoodsChatEvent event) {
switch (event.type()) {
case ENTER -> message += MEMBER_ENTER_MESSAGE;
case LEAVE -> message += MEMBER_LEAVE_MESSAGE;
case GOODS -> message += MEMBER_TRANSACTION_MESSAGE;
}
GoodsChatMessage chatMessage = createChatMessage(chatRoomId, member.getId(), message, event.type());

Expand Down Expand Up @@ -87,6 +91,6 @@ private GoodsChatRoom findByChatRoomById(Long chatRoomId) {
}

private void sendToSubscribers(Long chatRoomId, GoodsChatMessageResponse message) {
messagingTemplate.convertAndSend("/sub/chat/goods/" + chatRoomId, message);
messagingTemplate.convertAndSend(GOODS_CHAT_SUBSCRIBE_PATH + chatRoomId, message);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,14 @@
import com.example.mate.domain.goodsPost.entity.GoodsPost;
import com.example.mate.domain.goodsPost.entity.Role;
import com.example.mate.domain.goodsPost.entity.Status;
import com.example.mate.domain.goodsPost.event.GoodsPostEvent;
import com.example.mate.domain.goodsPost.event.GoodsPostEventPublisher;
import com.example.mate.domain.goodsPost.repository.GoodsPostRepository;
import com.example.mate.domain.member.dto.response.MemberSummaryResponse;
import com.example.mate.domain.member.entity.ActivityType;
import com.example.mate.domain.member.entity.Member;
import com.example.mate.domain.member.repository.MemberRepository;
import com.example.mate.domain.notification.entity.NotificationType;
import java.util.List;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
Expand All @@ -42,7 +46,8 @@ public class GoodsChatService {
private final GoodsChatRoomRepository chatRoomRepository;
private final GoodsChatPartRepository partRepository;
private final GoodsChatMessageRepository messageRepository;
private final GoodsChatEventPublisher eventPublisher;
private final GoodsChatEventPublisher chatEventPublisher;
private final GoodsPostEventPublisher notificationEventPublisher;

// 채팅방 생성 & 기존 채팅방 입장
public GoodsChatRoomResponse getOrCreateGoodsChatRoom(Long buyerId, Long goodsPostId) {
Expand Down Expand Up @@ -84,7 +89,7 @@ private GoodsChatRoomResponse createChatRoom(GoodsPost goodsPost, Member buyer,
savedChatRoom.addChatParticipant(seller, Role.SELLER);

// 입장 메시지 이벤트 전송
eventPublisher.publish(GoodsChatEvent.from(goodsChatRoom.getId(), buyer, MessageType.ENTER));
chatEventPublisher.publish(GoodsChatEvent.from(goodsChatRoom.getId(), buyer, MessageType.ENTER));

return GoodsChatRoomResponse.of(savedChatRoom, null);
}
Expand Down Expand Up @@ -170,13 +175,36 @@ public void deactivateGoodsChatPart(Long memberId, Long chatRoomId) {

if (!goodsChatPart.leaveAndCheckRoomStatus()) {
// 퇴장 메시지 전송
eventPublisher.publish(GoodsChatEvent.from(chatRoomId, member, MessageType.LEAVE));
chatEventPublisher.publish(GoodsChatEvent.from(chatRoomId, member, MessageType.LEAVE));
} else {
// 모두 나갔다면 채팅방, 채팅 참여, 채팅 삭제
deleteChatRoom(chatRoomId);
}
}

// 굿즈 거래완료
public void completeTransaction(Long sellerId, Long chatRoomId) {
Member seller = findMemberById(sellerId);
GoodsChatRoom chatRoom = findChatRoomById(chatRoomId);
Member buyer = getOpponentMember(chatRoom, seller);
GoodsPost goodsPost = chatRoom.getGoodsPost();

if (!goodsPost.getSeller().equals(seller)) {
throw new CustomException(ErrorCode.GOODS_MODIFICATION_NOT_ALLOWED);
}
if (goodsPost.getStatus() == Status.CLOSED) {
throw new CustomException(ErrorCode.GOODS_ALREADY_COMPLETED);
}

goodsPost.completeTransaction(buyer);
seller.updateManner(ActivityType.GOODS);
buyer.updateManner(ActivityType.GOODS);

// 거래완료 알림 및 채팅 메시지 전송
chatEventPublisher.publish(GoodsChatEvent.from(chatRoomId, seller, MessageType.GOODS));
notificationEventPublisher.publish(GoodsPostEvent.of(goodsPost.getId(), goodsPost.getTitle(), buyer, NotificationType.GOODS_CLOSED));
}

// 채팅방 삭제
private void deleteChatRoom(Long chatRoomId) {
chatRoomRepository.deleteById(chatRoomId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,15 +95,4 @@ public ResponseEntity<ApiResponse<PageResponse<GoodsPostSummaryResponse>>> getGo

return ResponseEntity.ok(ApiResponse.success(pageGoodsPosts));
}

@PostMapping("/{goodsPostId}/complete")
@Operation(summary = "굿즈 거래 완료", description = "굿즈거래 채팅방에서 굿즈거래를 거래완료 처리합니다.")
public ResponseEntity<ApiResponse<Void>> completeGoodsPost(
@AuthenticationPrincipal AuthMember member,
@Parameter(description = "판매글 ID", required = true) @PathVariable Long goodsPostId,
@Parameter(description = "구매자 ID", required = true) @RequestParam Long buyerId
) {
goodsPostService.completeTransaction(member.getMemberId(), goodsPostId, buyerId);
return ResponseEntity.ok(ApiResponse.success(null));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,11 @@
import com.example.mate.domain.goodsPost.entity.GoodsPost;
import com.example.mate.domain.goodsPost.entity.GoodsPostImage;
import com.example.mate.domain.goodsPost.entity.Status;
import com.example.mate.domain.goodsPost.event.GoodsPostEvent;
import com.example.mate.domain.goodsPost.event.GoodsPostEventPublisher;
import com.example.mate.domain.goodsPost.repository.GoodsPostImageRepository;
import com.example.mate.domain.goodsPost.repository.GoodsPostRepository;
import com.example.mate.domain.member.entity.ActivityType;
import com.example.mate.domain.member.entity.Member;
import com.example.mate.domain.member.repository.MemberRepository;
import com.example.mate.domain.notification.entity.NotificationType;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
Expand All @@ -41,7 +38,6 @@ public class GoodsPostService {
private final GoodsPostRepository goodsPostRepository;
private final GoodsPostImageRepository imageRepository;
private final FileService fileService;
private final GoodsPostEventPublisher eventPublisher;

public GoodsPostResponse registerGoodsPost(Long memberId, GoodsPostRequest request, List<MultipartFile> files) {
Member seller = findMemberById(memberId);
Expand All @@ -59,8 +55,7 @@ public GoodsPostResponse registerGoodsPost(Long memberId, GoodsPostRequest reque
return GoodsPostResponse.of(savedPost);
}

public GoodsPostResponse updateGoodsPost(Long memberId, Long goodsPostId, GoodsPostRequest request,
List<MultipartFile> files) {
public GoodsPostResponse updateGoodsPost(Long memberId, Long goodsPostId, GoodsPostRequest request, List<MultipartFile> files) {
Member seller = findMemberById(memberId);
GoodsPost goodsPost = findGoodsPostById(goodsPostId);

Expand Down Expand Up @@ -102,37 +97,25 @@ public GoodsPostResponse getGoodsPost(Long goodsPostId) {
@Transactional(readOnly = true)
public List<GoodsPostSummaryResponse> getMainGoodsPosts(Long teamId) {
validateTeamInfo(teamId);
return mapToGoodsPostSummaryResponses(
goodsPostRepository.findMainGoodsPosts(teamId, Status.OPEN, PageRequest.of(0, 4)));

Status status = Status.OPEN;
Pageable pageable = PageRequest.of(0, 4);

List<GoodsPost> mainGoodsPosts = goodsPostRepository.findMainGoodsPosts(teamId, status, pageable);
return mapToGoodsPostSummaryResponses(mainGoodsPosts);
}

@Transactional(readOnly = true)
public PageResponse<GoodsPostSummaryResponse> getPageGoodsPosts(Long teamId, String categoryVal,
Pageable pageable) {
public PageResponse<GoodsPostSummaryResponse> getPageGoodsPosts(Long teamId, String categoryVal, Pageable pageable) {
validateTeamInfo(teamId);

Category category = Category.from(categoryVal);
Page<GoodsPost> pageGoodsPosts = goodsPostRepository.findPageGoodsPosts(teamId, Status.OPEN, category,
pageable);
Status status = Status.OPEN;

Page<GoodsPost> pageGoodsPosts = goodsPostRepository.findPageGoodsPosts(teamId, status, category, pageable);
return PageResponse.from(pageGoodsPosts, mapToGoodsPostSummaryResponses(pageGoodsPosts.getContent()));
}

public void completeTransaction(Long sellerId, Long goodsPostId, Long buyerId) {
Member seller = findMemberById(sellerId);
Member buyer = findMemberById(buyerId);
GoodsPost goodsPost = findGoodsPostById(goodsPostId);

validateTransactionEligibility(seller, buyer, goodsPost);
goodsPost.completeTransaction(buyer);

seller.updateManner(ActivityType.GOODS);
buyer.updateManner(ActivityType.GOODS);

// 거래완료 알림 보내기
eventPublisher.publish(
GoodsPostEvent.of(goodsPost.getId(), goodsPost.getTitle(), buyer, NotificationType.GOODS_CLOSED));
}

private void attachImagesToGoodsPost(GoodsPost goodsPost, List<MultipartFile> files) {
List<GoodsPostImage> images = uploadImageFiles(files, goodsPost);
goodsPost.changeImages(images);
Expand Down Expand Up @@ -190,16 +173,4 @@ private void validateOwnership(Member seller, GoodsPost goodsPost) {
throw new CustomException(ErrorCode.GOODS_MODIFICATION_NOT_ALLOWED);
}
}

private void validateTransactionEligibility(Member seller, Member buyer, GoodsPost goodsPost) {
if (!goodsPost.getSeller().equals(seller)) {
throw new CustomException(ErrorCode.GOODS_MODIFICATION_NOT_ALLOWED);
}
if (seller.equals(buyer)) {
throw new CustomException(ErrorCode.SELLER_CANNOT_BE_BUYER);
}
if (goodsPost.getStatus() == Status.CLOSED) {
throw new CustomException(ErrorCode.GOODS_ALREADY_COMPLETED);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

Expand Down Expand Up @@ -386,4 +387,23 @@ void getGoodsChatRoomMembers_should_throw_exception_when_user_is_not_a_member()

verify(goodsChatService).getMembersInChatRoom(memberId, chatRoomId);
}

@Test
@DisplayName("굿즈 거래완료 - API 테스트")
void complete_goods_post_success() throws Exception {
// given
Long memberId = 1L;
Long chatRoomId = 1L;

willDoNothing().given(goodsChatService).completeTransaction(memberId, chatRoomId);

// when & then
mockMvc.perform(post("/api/goods/chat/{chatRoomId}/complete", chatRoomId))
.andDo(print())
.andExpect(status().isOk())
.andExpect(jsonPath("$.status").value("SUCCESS"))
.andExpect(jsonPath("$.code").value(200));

verify(goodsChatService).completeTransaction(memberId, chatRoomId);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.example.mate.domain.goodsChat.integration;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.within;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
Expand Down Expand Up @@ -123,7 +124,7 @@ void get_or_create_goods_chatroom_integration_test() throws Exception {
GoodsChatRoomResponse actualResponse = apiResponse.getData();

// then
GoodsChatRoom actualChatRoom = chatRoomRepository.findById(actualResponse.getChatRoomId()).orElse(null);
GoodsChatRoom actualChatRoom = chatRoomRepository.findById(actualResponse.getChatRoomId()).orElseThrow();
GoodsPost actualPost = actualChatRoom.getGoodsPost();
assertThat(actualPost.getId()).isEqualTo(goodsPost.getId());
assertThat(actualPost.getContent()).isEqualTo(goodsPost.getContent());
Expand Down Expand Up @@ -182,7 +183,7 @@ void get_goods_chat_room_info() throws Exception {
GoodsChatRoomResponse response = apiResponse.getData();

// then
GoodsChatRoom actualChatRoom = chatRoomRepository.findById(chatRoomId).orElse(null);
GoodsChatRoom actualChatRoom = chatRoomRepository.findById(chatRoomId).orElseThrow();
GoodsPost actualPost = actualChatRoom.getGoodsPost();

assertThat(response.getChatRoomId()).isEqualTo(chatRoomId);
Expand Down Expand Up @@ -347,6 +348,39 @@ void getGoodsChatRoomMembers_integration_test() throws Exception {
}
}

@Test
@DisplayName("굿즈 거래완료 통합 테스트")
@WithAuthMember
void complete_goods_post_integration_test() throws Exception {
// given
Long chatRoomId = chatRoom.getId();

// when
MockHttpServletResponse result = mockMvc.perform(post("/api/goods/chat/{chatRoomId}/complete", chatRoomId))
.andDo(print())
.andExpect(status().isOk())
.andReturn()
.getResponse();
result.setCharacterEncoding("UTF-8");

ApiResponse<Void> apiResponse = objectMapper.readValue(result.getContentAsString(), new TypeReference<>() {});

// then
assertThat(apiResponse.getCode()).isEqualTo(200);
assertThat(apiResponse.getStatus()).isEqualTo("SUCCESS");


GoodsPost completedPost = chatRoomRepository.findByChatRoomId(chatRoomId).orElseThrow().getGoodsPost();
assertThat(completedPost.getStatus()).isEqualTo(Status.CLOSED);
assertThat(completedPost.getBuyer()).isNotNull();

Member resultBuyer = completedPost.getBuyer();
assertThat(resultBuyer.getId()).isEqualTo(buyer.getId());
assertThat(resultBuyer.getName()).isEqualTo(buyer.getName());
assertThat(resultBuyer.getEmail()).isEqualTo(buyer.getEmail());
assertThat(resultBuyer.getNickname()).isEqualTo(buyer.getNickname());
}

private Member createMember(String name, String nickname, String email) {
return memberRepository.save(Member.builder()
.name(name)
Expand Down
Loading

0 comments on commit b73e4e6

Please sign in to comment.