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 #98] 채팅방 생성 API #107

Merged
merged 20 commits into from
Sep 14, 2024
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
535294e
[feat] : mongoDB 시간 UTC -> KST처럼 변환
hyun2371 Sep 2, 2024
fa63445
[style] : 메시지 응답 dto 필드명 수정
hyun2371 Sep 2, 2024
8f28c90
[feat] : 채팅방 아이디 응답 필드에서 삭제
hyun2371 Sep 2, 2024
7c03c9f
[feat] : 응답 DTO 필드에 메시지 생성 시간 추가
hyun2371 Sep 2, 2024
6cac33f
[feat] : chatRoomRepository 추가
hyun2371 Sep 5, 2024
0cbb486
[feat] : 채팅방 에러코드 추가
hyun2371 Sep 9, 2024
12eddef
[feat] : chatRoom 엔티티 수정
hyun2371 Sep 13, 2024
3658af5
[feat] : chatRoom 엔티티 필드명 수정
hyun2371 Sep 13, 2024
0458f9e
[rename] : request, response 패키지 추가해 분리
hyun2371 Sep 13, 2024
c2729f9
[feat] : chatMessageMapper로 분리
hyun2371 Sep 13, 2024
0221ed7
[feat] : 채팅방 생성, 상세조회 DTO 추가
hyun2371 Sep 13, 2024
bc6c807
[feat] : 채팅방 생성 비즈니스 로직 추가
hyun2371 Sep 13, 2024
876af4c
[test] : 채팅방 생성 비즈니스 로직 테스트
hyun2371 Sep 13, 2024
65f40ff
[feat] : 채팅방 생성 API 메서드 추가
hyun2371 Sep 13, 2024
56a66b8
[test] : 채팅방 생성 비즈니스 로직 예외 케이스 테스트
hyun2371 Sep 13, 2024
d71de7d
[feat] : 누락된 어노테이션 추가 및 수정
hyun2371 Sep 13, 2024
d20989b
[test] : 채팅방 생성 통합 테스트
hyun2371 Sep 13, 2024
b4fe525
[style] : 코드 리포멧팅
hyun2371 Sep 13, 2024
d8d667c
Merge branch 'dev' into feat/#98/create-chat-room
hyun2371 Sep 13, 2024
011d4d3
[feat] : dto 메시지 수정
hyun2371 Sep 14, 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
Expand Up @@ -6,8 +6,8 @@
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.stereotype.Controller;

import com.dnd.gongmuin.chat.dto.ChatMessageRequest;
import com.dnd.gongmuin.chat.dto.ChatMessageResponse;
import com.dnd.gongmuin.chat.dto.request.ChatMessageRequest;
import com.dnd.gongmuin.chat.dto.response.ChatMessageResponse;
import com.dnd.gongmuin.chat.service.ChatMessageService;

import lombok.RequiredArgsConstructor;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,23 @@

import org.springframework.data.domain.Pageable;
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.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import com.dnd.gongmuin.chat.dto.ChatMessageResponse;
import com.dnd.gongmuin.chat.dto.request.CreateChatRoomRequest;
import com.dnd.gongmuin.chat.dto.response.ChatMessageResponse;
import com.dnd.gongmuin.chat.dto.response.ChatRoomDetailResponse;
import com.dnd.gongmuin.chat.service.ChatRoomService;
import com.dnd.gongmuin.common.dto.PageResponse;
import com.dnd.gongmuin.member.domain.Member;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;

@Tag(name = "채팅방 API")
Expand All @@ -31,4 +38,14 @@ public ResponseEntity<PageResponse<ChatMessageResponse>> getChatMessages(
chatRoomService.getChatMessages(chatRoomId, pageable);
return ResponseEntity.ok(response);
}

@Operation(summary = "채팅방 생성 API", description = "요청자가 답변자와의 채팅방을 생성한다.")
@PostMapping("/api/chat-rooms")
public ResponseEntity<ChatRoomDetailResponse> createChatRoom(
@Valid @RequestBody CreateChatRoomRequest request,
@AuthenticationPrincipal Member member
) {
ChatRoomDetailResponse response = chatRoomService.createChatRoom(request, member);
return ResponseEntity.ok(response);
}
}
43 changes: 31 additions & 12 deletions src/main/java/com/dnd/gongmuin/chat/domain/ChatRoom.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
import static jakarta.persistence.FetchType.*;

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;
Expand All @@ -16,7 +18,6 @@
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

Expand All @@ -31,25 +32,43 @@ public class ChatRoom extends TimeBaseEntity {
private Long id;

@ManyToOne(fetch = LAZY)
@JoinColumn(name = "questioner_id", nullable = false,
@JoinColumn(name = "question_post_id",
nullable = false,
foreignKey = @ForeignKey(NO_CONSTRAINT))
private Member questioner;
private QuestionPost questionPost;

@ManyToOne(fetch = LAZY)
@JoinColumn(name = "answerer_id", nullable = false,
@JoinColumn(name = "inquirer_id", nullable = false,
foreignKey = @ForeignKey(NO_CONSTRAINT))
private Member answerer;
private Member inquirer;

@ManyToOne(fetch = LAZY)
@JoinColumn(name = "question_post_id",
nullable = false,
@JoinColumn(name = "answerer_id", nullable = false,
foreignKey = @ForeignKey(NO_CONSTRAINT))
private QuestionPost questionPost;
private Member answerer;

@Builder
public ChatRoom(Member questioner, Member answerer, QuestionPost questionPost) {
this.questioner = questioner;
this.answerer = answerer;
@Column(name = "is_accepted", nullable = false)
private boolean isAccepted;

private ChatRoom(QuestionPost questionPost, Member inquirer, Member answerer) {
this.questionPost = questionPost;
this.inquirer = inquirer;
this.answerer = answerer;
this.isAccepted = false;
validateInquirerCredit();
}

public static ChatRoom of(
QuestionPost questionPost,
Member inquirer,
Member answerer
) {
return new ChatRoom(questionPost, inquirer, answerer);
}

private void validateInquirerCredit() {
if (inquirer.getCredit() < 2000) {
throw new ValidationException(MemberErrorCode.NOT_ENOUGH_CREDIT);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,23 @@

import com.dnd.gongmuin.chat.domain.ChatMessage;
import com.dnd.gongmuin.chat.domain.MessageType;
import com.dnd.gongmuin.chat.dto.request.ChatMessageRequest;
import com.dnd.gongmuin.chat.dto.response.ChatMessageResponse;

import lombok.AccessLevel;
import lombok.NoArgsConstructor;

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class ChatMapper {
public class ChatMessageMapper {

public static ChatMessageResponse toChatMessageResponse(
ChatMessage chatMessage
) {
return new ChatMessageResponse(
chatMessage.getMemberId(),
chatMessage.getChatRoomId(),
chatMessage.getContent(),
chatMessage.getType().getLabel()
chatMessage.getType().getLabel(),
chatMessage.getCreatedAt().toString()
);
}

Expand Down

This file was deleted.

44 changes: 44 additions & 0 deletions src/main/java/com/dnd/gongmuin/chat/dto/ChatRoomMapper.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.dnd.gongmuin.chat.dto;

import com.dnd.gongmuin.chat.domain.ChatRoom;
import com.dnd.gongmuin.chat.dto.response.ChatRoomDetailResponse;
import com.dnd.gongmuin.member.domain.Member;
import com.dnd.gongmuin.question_post.domain.QuestionPost;
import com.dnd.gongmuin.question_post.dto.response.MemberInfo;

import lombok.AccessLevel;
import lombok.NoArgsConstructor;

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class ChatRoomMapper {

public static ChatRoom toChatRoom(
QuestionPost questionPost,
Member inquirer,
Member answerer
) {
return ChatRoom.of(
questionPost,
inquirer,
answerer
);
}

public static ChatRoomDetailResponse toChatRoomDetailResponse(ChatRoom chatRoom) {
QuestionPost questionPost = chatRoom.getQuestionPost();
Member answerer = chatRoom.getAnswerer(); // 요청자만 채팅방 생성 가능 -> 상태방: 답변자

return new ChatRoomDetailResponse(
questionPost.getId(),
questionPost.getJobGroup().getLabel(),
questionPost.getTitle(),
new MemberInfo(
answerer.getId(),
answerer.getNickname(),
answerer.getJobGroup().getLabel(),
answerer.getProfileImageNo()
),
chatRoom.isAccepted()
);
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.dnd.gongmuin.chat.dto;
package com.dnd.gongmuin.chat.dto.request;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.dnd.gongmuin.chat.dto.request;

import jakarta.validation.constraints.NotNull;

public record CreateChatRoomRequest(
@NotNull(message = "질문 게시글 아이디를 입력해주세요.")
Long questionPostId,

@NotNull(message = "답변자 아이디를 입력해주세요.")
Long answererId
) {
hyun2371 marked this conversation as resolved.
Show resolved Hide resolved
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.dnd.gongmuin.chat.dto.response;

public record ChatMessageResponse(
Long senderId,
String content,
String type,
String createdAt
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.dnd.gongmuin.chat.dto.response;

import com.dnd.gongmuin.question_post.dto.response.MemberInfo;

public record ChatRoomDetailResponse(
Long questionPostId,
String targetJobGroup,
String title,
MemberInfo receiverInfo,
boolean isAccepted
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
@RequiredArgsConstructor
public enum ChatErrorCode implements ErrorCode {

INVALID_MESSAGE_TYPE("메시지 타입을 올바르게 입력해주세요.", "CH_001");
INVALID_MESSAGE_TYPE("메시지 타입을 올바르게 입력해주세요.", "CH_001"),
NOT_FOUND_CHAT_ROOM("해당 아이디의 채팅방이 존재하지 않습니다.", "CH_002");

private final String message;
private final String code;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.dnd.gongmuin.chat.repository;

import org.springframework.data.jpa.repository.JpaRepository;

import com.dnd.gongmuin.chat.domain.ChatRoom;

public interface ChatRoomRepository extends JpaRepository<ChatRoom, Long> {
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
import org.springframework.transaction.annotation.Transactional;

import com.dnd.gongmuin.chat.domain.ChatMessage;
import com.dnd.gongmuin.chat.dto.ChatMapper;
import com.dnd.gongmuin.chat.dto.ChatMessageRequest;
import com.dnd.gongmuin.chat.dto.ChatMessageResponse;
import com.dnd.gongmuin.chat.dto.ChatMessageMapper;
import com.dnd.gongmuin.chat.dto.request.ChatMessageRequest;
import com.dnd.gongmuin.chat.dto.response.ChatMessageResponse;
import com.dnd.gongmuin.chat.repository.ChatMessageRepository;

import lombok.RequiredArgsConstructor;
Expand All @@ -25,8 +25,9 @@ public ChatMessageResponse saveChatMessage(
Long chatRoomId
) {
Long memberId = request.memberId();
ChatMessage chatMessage = chatMessageRepository.save(ChatMapper.toChatMessage(request, chatRoomId, memberId));
ChatMessage chatMessage = chatMessageRepository.save(
ChatMessageMapper.toChatMessage(request, chatRoomId, memberId));
log.info("chatRoomId = {}, memberId= {}, chatMessageId= {}", chatRoomId, memberId, chatMessage.getId());
return ChatMapper.toChatMessageResponse(chatMessage);
return ChatMessageMapper.toChatMessageResponse(chatMessage);
}
}
39 changes: 36 additions & 3 deletions src/main/java/com/dnd/gongmuin/chat/service/ChatRoomService.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,22 @@
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.dnd.gongmuin.chat.dto.ChatMapper;
import com.dnd.gongmuin.chat.dto.ChatMessageResponse;
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.ChatMessageResponse;
import com.dnd.gongmuin.chat.dto.response.ChatRoomDetailResponse;
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.member.domain.Member;
import com.dnd.gongmuin.member.exception.MemberErrorCode;
import com.dnd.gongmuin.member.repository.MemberRepository;
import com.dnd.gongmuin.question_post.domain.QuestionPost;
import com.dnd.gongmuin.question_post.exception.QuestionPostErrorCode;
import com.dnd.gongmuin.question_post.repository.QuestionPostRepository;

import lombok.RequiredArgsConstructor;

Expand All @@ -18,13 +29,35 @@
public class ChatRoomService {

private final ChatMessageRepository chatMessageRepository;
private final ChatRoomRepository chatRoomRepository;
private final MemberRepository memberRepository;
private final QuestionPostRepository questionPostRepository;

@Transactional(readOnly = true)
public PageResponse<ChatMessageResponse> getChatMessages(Long chatRoomId, Pageable pageable) {
dudxo marked this conversation as resolved.
Show resolved Hide resolved
Slice<ChatMessageResponse> responsePage = chatMessageRepository
.findByChatRoomIdOrderByCreatedAtDesc(chatRoomId, pageable)
.map(ChatMapper::toChatMessageResponse);
.map(ChatMessageMapper::toChatMessageResponse);
return PageMapper.toPageResponse(responsePage);
}

@Transactional
public ChatRoomDetailResponse createChatRoom(CreateChatRoomRequest request, Member inquirer) {
QuestionPost questionPost = getQuestionPostById(request.questionPostId());
Member answerer = getMemberById(request.answererId());
return ChatRoomMapper.toChatRoomDetailResponse(
chatRoomRepository.save(ChatRoomMapper.toChatRoom(questionPost, inquirer, answerer))
);
}

private QuestionPost getQuestionPostById(Long id) {
return questionPostRepository.findById(id)
.orElseThrow(() -> new NotFoundException(QuestionPostErrorCode.NOT_FOUND_QUESTION_POST));
}

private Member getMemberById(Long id) {
return memberRepository.findById(id)
.orElseThrow(() -> new NotFoundException(MemberErrorCode.NOT_FOUND_MEMBER));
}
}

25 changes: 25 additions & 0 deletions src/main/java/com/dnd/gongmuin/common/config/MongoConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.dnd.gongmuin.common.config;

import java.util.Arrays;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.mongodb.core.convert.MongoCustomConversions;

import com.dnd.gongmuin.converter.DateToLocalDateTimeKstConverter;
import com.dnd.gongmuin.converter.LocalDateTimeToDateKstConverter;

@Configuration
public class MongoConfig {

@Bean
public MongoCustomConversions customConversions(
LocalDateTimeToDateKstConverter localDateTimeToDateKstConverter,
DateToLocalDateTimeKstConverter dateToLocalDateTimeKstConverter) {

return new MongoCustomConversions(Arrays.asList(
localDateTimeToDateKstConverter,
dateToLocalDateTimeKstConverter
));
}
}
Loading
Loading