Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feat #108] 채팅 수락/거절 API #109

Merged
merged 23 commits into from
Sep 18, 2024
Merged
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
5781fd9
[feat] : 채팅 요청 시 크레딧 차감
hyun2371 Sep 14, 2024
375f50a
[feat] : 채팅방 수락 여부 필드 -> 상태 enum으로 변경
hyun2371 Sep 14, 2024
0dc44ea
[feat] : 채팅방 수락 응답 DTO 추가
hyun2371 Sep 16, 2024
6748dc4
[feat] : 채팅방 수락 관련 에러코드 추가
hyun2371 Sep 16, 2024
87f19b6
[feat] : 채팅방 엔티티 -> 응답 변환 매퍼 함수 추가
hyun2371 Sep 16, 2024
fae7738
[feat] : 채팅방 상태 변경 함수 추가
hyun2371 Sep 16, 2024
d28dc7b
[feat] : 채팅 보상 크레딧 상수로 추출
hyun2371 Sep 16, 2024
48d3836
[feat] : 채팅 수락 비즈니스 로직 작성
hyun2371 Sep 16, 2024
8962821
[test] : 채팅 수락 비즈니스 로직 테스트
hyun2371 Sep 16, 2024
1200c16
[feat] : 채팅 수락 컨트롤러 메서드 작성
hyun2371 Sep 16, 2024
19f6cad
[feat] : 답변자와 멤버 비교 시 아이디로 동일성 비교
hyun2371 Sep 16, 2024
4495385
[test] : 채팅방 수락 로직 통합테스트
hyun2371 Sep 16, 2024
19d3a75
[test] : 각 테스트 끝날 때마다 DB 초기화
hyun2371 Sep 16, 2024
9fd756f
[test] : 실제 저장되는 채팅방 아이디 할당
hyun2371 Sep 16, 2024
d7fccf1
[feat] : 채팅 상태 ENUM 필드명 변경
hyun2371 Sep 16, 2024
ea70e92
[style] : 채팅 오류 ENUM 필드명 변경
hyun2371 Sep 16, 2024
815757d
[feat] : 채팅 거절 시 상태변경 메서드 추가
hyun2371 Sep 16, 2024
45e97e8
[feat] : 채팅 거절 응답 dto 추가
hyun2371 Sep 16, 2024
7f2df72
[feat] : 채팅 거절 비즈니스 메서드 추가
hyun2371 Sep 16, 2024
f66c595
[feat] : 채팅 거절 controller 메서드 추가
hyun2371 Sep 16, 2024
46bf9d0
[test] : 채팅 거절 비즈니스 로직 테스트
hyun2371 Sep 16, 2024
2ef0f9f
[test] : 채팅 거절 로직 통합 테스트
hyun2371 Sep 16, 2024
ab69788
[style] : 코드 리포멧팅
hyun2371 Sep 16, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -4,14 +4,17 @@
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.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import com.dnd.gongmuin.chat.dto.request.CreateChatRoomRequest;
import com.dnd.gongmuin.chat.dto.response.AcceptChatResponse;
import com.dnd.gongmuin.chat.dto.response.ChatMessageResponse;
import com.dnd.gongmuin.chat.dto.response.ChatRoomDetailResponse;
import com.dnd.gongmuin.chat.dto.response.RejectChatResponse;
import com.dnd.gongmuin.chat.service.ChatRoomService;
import com.dnd.gongmuin.common.dto.PageResponse;
import com.dnd.gongmuin.member.domain.Member;
@@ -48,4 +51,24 @@ public ResponseEntity<ChatRoomDetailResponse> createChatRoom(
ChatRoomDetailResponse response = chatRoomService.createChatRoom(request, member);
return ResponseEntity.ok(response);
}

@Operation(summary = "채팅 수락 API", description = "채팅방에서 요청자와의 채팅을 수락한다.")
@PatchMapping("/api/chat-rooms/{chatRoomId}/accept")
public ResponseEntity<AcceptChatResponse> acceptChat(
@PathVariable("chatRoomId") Long chatRoomId,
@AuthenticationPrincipal Member member
) {
AcceptChatResponse response = chatRoomService.acceptChat(chatRoomId, member);
return ResponseEntity.ok(response);
}

@Operation(summary = "채팅 거절 API", description = "채팅방에서 요청자와의 채팅을 거절한다.")
@PatchMapping("/api/chat-rooms/{chatRoomId}/reject")
public ResponseEntity<RejectChatResponse> rejectChat(
@PathVariable("chatRoomId") Long chatRoomId,
@AuthenticationPrincipal Member member
) {
RejectChatResponse response = chatRoomService.rejectChat(chatRoomId, member);
return ResponseEntity.ok(response);
}
}
31 changes: 23 additions & 8 deletions src/main/java/com/dnd/gongmuin/chat/domain/ChatRoom.java
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
package com.dnd.gongmuin.chat.domain;

import static jakarta.persistence.ConstraintMode.*;
import static jakarta.persistence.EnumType.*;
import static jakarta.persistence.FetchType.*;

import com.dnd.gongmuin.chat.exception.ChatErrorCode;
import com.dnd.gongmuin.common.entity.TimeBaseEntity;
import com.dnd.gongmuin.common.exception.runtime.ValidationException;
import com.dnd.gongmuin.member.domain.Member;
import com.dnd.gongmuin.member.exception.MemberErrorCode;
import com.dnd.gongmuin.question_post.domain.QuestionPost;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Enumerated;
import jakarta.persistence.ForeignKey;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
@@ -26,6 +28,8 @@
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class ChatRoom extends TimeBaseEntity {

private static final int CHAT_REWARD = 2000;

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "chat_room_id", nullable = false)
@@ -47,15 +51,16 @@ public class ChatRoom extends TimeBaseEntity {
foreignKey = @ForeignKey(NO_CONSTRAINT))
private Member answerer;

@Column(name = "is_accepted", nullable = false)
private boolean isAccepted;
@Enumerated(STRING)
@Column(name = "status", nullable = false)
private ChatStatus status;

private ChatRoom(QuestionPost questionPost, Member inquirer, Member answerer) {
this.questionPost = questionPost;
this.inquirer = inquirer;
this.answerer = answerer;
this.isAccepted = false;
validateInquirerCredit();
this.status = ChatStatus.PENDING;
inquirer.decreaseCredit(CHAT_REWARD);
}

public static ChatRoom of(
@@ -66,9 +71,19 @@ public static ChatRoom of(
return new ChatRoom(questionPost, inquirer, answerer);
}

private void validateInquirerCredit() {
if (inquirer.getCredit() < 2000) {
throw new ValidationException(MemberErrorCode.NOT_ENOUGH_CREDIT);
public void updateStatusAccepted() {
if (status != ChatStatus.PENDING) {
throw new ValidationException(ChatErrorCode.UNABLE_TO_CHANGE_CHAT_STATUS);
}
status = ChatStatus.ACCEPTED;
answerer.increaseCredit(CHAT_REWARD);
}

public void updateStatusRejected() {
if (status != ChatStatus.PENDING) {
throw new ValidationException(ChatErrorCode.UNABLE_TO_CHANGE_CHAT_STATUS);
}
status = ChatStatus.REJECTED;
inquirer.increaseCredit(CHAT_REWARD);
}
}
15 changes: 15 additions & 0 deletions src/main/java/com/dnd/gongmuin/chat/domain/ChatStatus.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.dnd.gongmuin.chat.domain;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public enum ChatStatus {

PENDING("요청중"),
ACCEPTED("수락됨"),
REJECTED("거절됨");

private final String label;
}
18 changes: 17 additions & 1 deletion src/main/java/com/dnd/gongmuin/chat/dto/ChatRoomMapper.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package com.dnd.gongmuin.chat.dto;

import com.dnd.gongmuin.chat.domain.ChatRoom;
import com.dnd.gongmuin.chat.dto.response.AcceptChatResponse;
import com.dnd.gongmuin.chat.dto.response.ChatRoomDetailResponse;
import com.dnd.gongmuin.chat.dto.response.RejectChatResponse;
import com.dnd.gongmuin.member.domain.Member;
import com.dnd.gongmuin.question_post.domain.QuestionPost;
import com.dnd.gongmuin.question_post.dto.response.MemberInfo;
@@ -38,7 +40,21 @@ public static ChatRoomDetailResponse toChatRoomDetailResponse(ChatRoom chatRoom)
answerer.getJobGroup().getLabel(),
answerer.getProfileImageNo()
),
chatRoom.isAccepted()
chatRoom.getStatus().getLabel()
);
}

public static AcceptChatResponse toAcceptChatResponse(ChatRoom chatRoom) {
return new AcceptChatResponse(
chatRoom.getStatus().getLabel(),
chatRoom.getAnswerer().getCredit()
);
}

public static RejectChatResponse toRejectChatResponse(ChatRoom chatRoom) {
return new RejectChatResponse(
chatRoom.getStatus().getLabel()
);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.dnd.gongmuin.chat.dto.response;

public record AcceptChatResponse(
String chatStatus,
int credit
) {
}
Original file line number Diff line number Diff line change
@@ -7,6 +7,6 @@ public record ChatRoomDetailResponse(
String targetJobGroup,
String title,
MemberInfo receiverInfo,
boolean isAccepted
String status
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.dnd.gongmuin.chat.dto.response;

public record RejectChatResponse(
String chatStatus
) {
}
Original file line number Diff line number Diff line change
@@ -10,7 +10,9 @@
public enum ChatErrorCode implements ErrorCode {

INVALID_MESSAGE_TYPE("메시지 타입을 올바르게 입력해주세요.", "CH_001"),
NOT_FOUND_CHAT_ROOM("해당 아이디의 채팅방이 존재하지 않습니다.", "CH_002");
NOT_FOUND_CHAT_ROOM("해당 아이디의 채팅방이 존재하지 않습니다.", "CH_002"),
UNAUTHORIZED_REQUEST("채팅 수락을 하거나 거절할 권한이 없습니다.", "CH_003"),
UNABLE_TO_CHANGE_CHAT_STATUS("이미 수락했거나 거절한 요청입니다.", "CH_004");

private final String message;
private final String code;
36 changes: 36 additions & 0 deletions src/main/java/com/dnd/gongmuin/chat/service/ChatRoomService.java
Original file line number Diff line number Diff line change
@@ -1,20 +1,27 @@
package com.dnd.gongmuin.chat.service;

import java.util.Objects;

import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.dnd.gongmuin.chat.domain.ChatRoom;
import com.dnd.gongmuin.chat.dto.ChatMessageMapper;
import com.dnd.gongmuin.chat.dto.ChatRoomMapper;
import com.dnd.gongmuin.chat.dto.request.CreateChatRoomRequest;
import com.dnd.gongmuin.chat.dto.response.AcceptChatResponse;
import com.dnd.gongmuin.chat.dto.response.ChatMessageResponse;
import com.dnd.gongmuin.chat.dto.response.ChatRoomDetailResponse;
import com.dnd.gongmuin.chat.dto.response.RejectChatResponse;
import com.dnd.gongmuin.chat.exception.ChatErrorCode;
import com.dnd.gongmuin.chat.repository.ChatMessageRepository;
import com.dnd.gongmuin.chat.repository.ChatRoomRepository;
import com.dnd.gongmuin.common.dto.PageMapper;
import com.dnd.gongmuin.common.dto.PageResponse;
import com.dnd.gongmuin.common.exception.runtime.NotFoundException;
import com.dnd.gongmuin.common.exception.runtime.ValidationException;
import com.dnd.gongmuin.member.domain.Member;
import com.dnd.gongmuin.member.exception.MemberErrorCode;
import com.dnd.gongmuin.member.repository.MemberRepository;
@@ -33,6 +40,12 @@ public class ChatRoomService {
private final MemberRepository memberRepository;
private final QuestionPostRepository questionPostRepository;

private static void validateIfAnswerer(Member member, ChatRoom chatRoom) {
if (!Objects.equals(member.getId(), chatRoom.getAnswerer().getId())) {
throw new ValidationException(ChatErrorCode.UNAUTHORIZED_REQUEST);
}
}

@Transactional(readOnly = true)
public PageResponse<ChatMessageResponse> getChatMessages(Long chatRoomId, Pageable pageable) {
Slice<ChatMessageResponse> responsePage = chatMessageRepository
@@ -50,6 +63,29 @@ public ChatRoomDetailResponse createChatRoom(CreateChatRoomRequest request, Memb
);
}

@Transactional
public AcceptChatResponse acceptChat(Long chatRoomId, Member member) {
ChatRoom chatRoom = getChatRoomById(chatRoomId);
validateIfAnswerer(member, chatRoom);
chatRoom.updateStatusAccepted();

return ChatRoomMapper.toAcceptChatResponse(chatRoom);
}

@Transactional
public RejectChatResponse rejectChat(Long chatRoomId, Member member) {
ChatRoom chatRoom = getChatRoomById(chatRoomId);
validateIfAnswerer(member, chatRoom);
chatRoom.updateStatusRejected();

return ChatRoomMapper.toRejectChatResponse(chatRoom);
}

private ChatRoom getChatRoomById(Long id) {
return chatRoomRepository.findById(id)
.orElseThrow(() -> new NotFoundException(ChatErrorCode.NOT_FOUND_CHAT_ROOM));
}

private QuestionPost getQuestionPostById(Long id) {
return questionPostRepository.findById(id)
.orElseThrow(() -> new NotFoundException(QuestionPostErrorCode.NOT_FOUND_QUESTION_POST));
Original file line number Diff line number Diff line change
@@ -6,14 +6,19 @@

import java.util.List;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;

import com.dnd.gongmuin.chat.domain.ChatMessage;
import com.dnd.gongmuin.chat.domain.ChatRoom;
import com.dnd.gongmuin.chat.domain.ChatStatus;
import com.dnd.gongmuin.chat.dto.request.CreateChatRoomRequest;
import com.dnd.gongmuin.chat.repository.ChatMessageRepository;
import com.dnd.gongmuin.chat.repository.ChatRoomRepository;
import com.dnd.gongmuin.common.fixture.ChatMessageFixture;
import com.dnd.gongmuin.common.fixture.ChatRoomFixture;
import com.dnd.gongmuin.common.fixture.MemberFixture;
import com.dnd.gongmuin.common.fixture.QuestionPostFixture;
import com.dnd.gongmuin.common.support.ApiTestSupport;
@@ -25,6 +30,8 @@
@DisplayName("[ChatMessage 통합 테스트]")
class ChatRoomControllerTest extends ApiTestSupport {

private static final int CHAT_REWARD = 2000;

@Autowired
private ChatMessageRepository chatMessageRepository;

@@ -34,6 +41,16 @@ class ChatRoomControllerTest extends ApiTestSupport {
@Autowired
private QuestionPostRepository questionPostRepository;

@Autowired
private ChatRoomRepository chatRoomRepository;

@AfterEach
void teardown() {
memberRepository.deleteAll();
questionPostRepository.deleteAll();
chatRoomRepository.deleteAll();
}

@DisplayName("[채팅방 아이디로 메시지를 조회할 수 있다.]")
@Test
void getChatMessages() throws Exception {
@@ -64,4 +81,31 @@ void createChatRoom() throws Exception {
.andExpect(status().isOk());
}

@DisplayName("[답변자가 채팅 요청을 수락할 수 있다.]")
@Test
void acceptChatRoom() throws Exception {
Member inquirer = memberRepository.save(MemberFixture.member4());
QuestionPost questionPost = questionPostRepository.save(QuestionPostFixture.questionPost(inquirer));
ChatRoom chatRoom = chatRoomRepository.save(ChatRoomFixture.chatRoom(questionPost, inquirer, loginMember));
int previousAnswererCredit = chatRoom.getAnswerer().getCredit();

mockMvc.perform(patch("/api/chat-rooms/{chatRoomId}/accept", chatRoom.getId())
.cookie(accessToken))
.andExpect(status().isOk())
.andExpect(jsonPath("$.chatStatus").value(ChatStatus.ACCEPTED.getLabel()))
.andExpect(jsonPath("$.credit").value(previousAnswererCredit + CHAT_REWARD));
}

@DisplayName("[답변자가 채팅 요청을 거절할 수 있다.]")
@Test
void rejectChatRoom() throws Exception {
Member inquirer = memberRepository.save(MemberFixture.member4());
QuestionPost questionPost = questionPostRepository.save(QuestionPostFixture.questionPost(inquirer));
ChatRoom chatRoom = chatRoomRepository.save(ChatRoomFixture.chatRoom(questionPost, inquirer, loginMember));

mockMvc.perform(patch("/api/chat-rooms/{chatRoomId}/reject", chatRoom.getId())
.cookie(accessToken))
.andExpect(status().isOk())
.andExpect(jsonPath("$.chatStatus").value(ChatStatus.REJECTED.getLabel()));
}
}
Original file line number Diff line number Diff line change
@@ -19,9 +19,12 @@

import com.dnd.gongmuin.chat.domain.ChatMessage;
import com.dnd.gongmuin.chat.domain.ChatRoom;
import com.dnd.gongmuin.chat.domain.ChatStatus;
import com.dnd.gongmuin.chat.dto.request.CreateChatRoomRequest;
import com.dnd.gongmuin.chat.dto.response.AcceptChatResponse;
import com.dnd.gongmuin.chat.dto.response.ChatMessageResponse;
import com.dnd.gongmuin.chat.dto.response.ChatRoomDetailResponse;
import com.dnd.gongmuin.chat.dto.response.RejectChatResponse;
import com.dnd.gongmuin.chat.repository.ChatMessageRepository;
import com.dnd.gongmuin.chat.repository.ChatRoomRepository;
import com.dnd.gongmuin.common.exception.runtime.ValidationException;
@@ -39,8 +42,8 @@
@ExtendWith(MockitoExtension.class)
class ChatRoomServiceTest {

private static final int CHAT_REWARD = 2000;
private final PageRequest pageRequest = PageRequest.of(0, 5);

@Mock
private ChatMessageRepository chatMessageRepository;

@@ -126,4 +129,51 @@ void createChatRoom_fail() {
.isInstanceOf(ValidationException.class)
.hasMessageContaining(MemberErrorCode.NOT_ENOUGH_CREDIT.getMessage());
}

@DisplayName("[답변자가 채팅 요청을 수락할 수 있다.]")
@Test
void acceptChat() {
//given
Long chatRoomId = 1L;
Member inquirer = MemberFixture.member(1L);
Member answerer = MemberFixture.member(2L);
int previousCredit = answerer.getCredit();
QuestionPost questionPost = QuestionPostFixture.questionPost(inquirer);
ChatRoom chatRoom = ChatRoomFixture.chatRoom(questionPost, inquirer, answerer);

given(chatRoomRepository.findById(chatRoomId))
.willReturn(Optional.of(chatRoom));

//when
AcceptChatResponse response = chatRoomService.acceptChat(chatRoomId, answerer);

//then
assertAll(
() -> assertThat(response.chatStatus())
.isEqualTo(ChatStatus.ACCEPTED.getLabel()),
() -> assertThat(response.credit())
.isEqualTo(previousCredit + CHAT_REWARD)
);
}

@DisplayName("[답변자가 채팅 요청을 거절할 수 있다.]")
@Test
void rejectChat() {
//given
Long chatRoomId = 1L;
Member inquirer = MemberFixture.member(1L);
Member answerer = MemberFixture.member(2L);
QuestionPost questionPost = QuestionPostFixture.questionPost(inquirer);
ChatRoom chatRoom = ChatRoomFixture.chatRoom(questionPost, inquirer, answerer);

given(chatRoomRepository.findById(chatRoomId))
.willReturn(Optional.of(chatRoom));

//when
RejectChatResponse response = chatRoomService.rejectChat(chatRoomId, answerer);

//then
assertThat(response.chatStatus())
.isEqualTo(ChatStatus.REJECTED.getLabel());
}
}