본문 바로가기
프로젝트/AI명종원

채팅 CRUD 기능 구현

by HWK 2024. 5. 18.

이전 글: GPT-4: 재료->음식->레시피 기능 구현 (tistory.com)

레시피 CRUD 기능 구현 (tistory.com)

서비스를 채팅 형식으로 제공 할 것이기 때문에, 채팅에 관한 기능을 만들었다.

아래는 채팅에 관한 er다이어그램이다.

모든 채팅을 보여주기 위해 chat_room이라는 table를 만들었고, chat_message는 채팅에 관한 상세 정보를 가지고 있다.

1. Entity 설명

public class ChatRoom extends Timestamped{ // created_at 알아서 생성해줌
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "chat_id")
    private Long id;
    private String menu;
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id")
    private User user;
    @OneToMany(mappedBy = "chatRoom", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<ChatMessage> chatMessages = new ArrayList<>();

    public ChatRoom(User user, String menu) {
        this.user = user;
        this.menu = menu;
    }
}

chatRoom에서 Timestamped 라는 클래스를 상속받아 created_at이 자동적으로 생성된다.

Timestamped 클래스

더보기
@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class Timestamped {
    @CreatedDate
    @Column(updatable = false)
    @Temporal(TemporalType.TIMESTAMP)
    private LocalDateTime createdAt;
}
    • @MappedSuperclass: JPA 어노테이션으로, 이 클래스를 직접 엔티티로 사용하지 않고, 다른 엔티티 클래스들이 상속받을 수 있도록 해줌. 이 클래스에 정의된 매핑 정보가 상속받는 엔티티 클래스에 적용된다.
    • @EntityListeners(AuditingEntityListener.class): JPA 엔티티 리스너를 지정해준다. AuditingEntityListener는 스프링 데이터 JPA의 감시 기능을 사용하여 엔티티의 생성 및 수정 시점을 자동으로 관리할 수 있도록 해준다.
    • @CreatedDate: 스프링 데이터 JPA의 어노테이션으로, 엔티티가 처음 생성될 때 생성 날짜를 자동으로 설정해주며, AuditingEntityListener와 함께 사용한다.
    • @Column(updatable = false):JPA 어노테이션으로, 데이터베이스 칼럼을 수정할 수 없도록 설정한다.

사용하기 위해서는 아래처럼 @EnableJpaAuditing을 추가해야 함

@SpringBootApplication
@EnableJpaAuditing // timestamp 사용하기 위함
public class AiMjuApplication {
    public static void main(String[] args) {
        SpringApplication.run(AiMjuApplication.class, args);
    }
}
public class ChatMessage {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String content;
    private Long isUser;
    //    @Em
    private ChatType chatType;
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "chat_id")
    private ChatRoom chatRoom;

    public ChatMessage(String content, Long isUser, ChatType chatType, ChatRoom chatRoom) {
        this.content = content;
        this.isUser = isUser;
        this.chatType = chatType;
        this.chatRoom = chatRoom;
    }
}

채팅의 상세적인 내용을 저장하며, content가 내용, isUser는 user가 쓴 채팅인지 아닌지, chatType는 아래와 같다.

public enum ChatType {
    message,image,menu,ingredient,recipe,link
}

말그대로 어떤 Type의 채팅인지 지정한다.

2. 채팅 저장

public void saveChatData(ChatRoomRequestDto chatRoomRequestDto, HttpSession session) {
    // ChatRoom 데이터 저장
    ChatRoom chatRoom = new ChatRoom(getUserFromSession(session), chatRoomRequestDto.getMenu());
    chatRoomRepository.save(chatRoom);

    // ChatMessage 데이터 저장
    List<ChatMessage> chatMessages = new ArrayList<>();
    for (ChatMessageRequestDto chatMessageRequestDto : chatRoomRequestDto.getMessages()) {
        ChatMessage chatMessage = new ChatMessage(
                chatMessageRequestDto.getContent(),
                chatMessageRequestDto.getIsUser(),
                chatMessageRequestDto.getChatType(),
                chatRoom);
        chatMessages.add(chatMessage);
    }
    chatMessageRepository.saveAll(chatMessages);
}

전달받은 Dto 형식을 ChatMessage로 변환하여 한번에 저장한다.

아래는 Dto들의 형식이다.

public class ChatRoomRequestDto {
    private String menu;
    private List<ChatMessageRequestDto> messages;
}

public class ChatMessageRequestDto {
    private String content;
    private Long isUser;
    private ChatType chatType;
}

위처럼 ChatType도 나누고 isUser도 나눈 이유는 나중에 채팅기록을 다시 보여주기 위해서이다.

하지만 지금 생각해보면 isUser는 굳이 필요하지 않을것 같다. ChatType도 필요 없을 수도 있겠다는 생각이 든다.

어짜피 대화 형식이 지정되어 있어, id 순서대로 잘 정렬만 해주면 채팅 기록을 되살릴 수 있기 때문이다.

3. 채팅 보기

public List<GetAllChatRoomResponseDto> getAllChatRooms(HttpSession session) {
    List<ChatRoom> chatRoomList = chatRoomRepository.findAllByUserId(getUserFromSession(session).getId());
    return chatRoomList.stream()
            .map(this::mapToChatRoomDto)
            .collect(Collectors.toList());
}

public List<GetAllChatMessageResponseDto> getAllChatMessages(Long chatId) {
    // 채팅방 ID로 해당 채팅방의 모든 메시지 조회
    List<ChatMessage> chatMessages = chatMessageRepository.findAllByChatRoomId(chatId);

    // ChatMessage를 GetAllChatMessageResponseDto로 변환
    return chatMessages.stream()
            .map(this::mapToMessageDto)
            .collect(Collectors.toList());
}

getAllChatRoom() 메서드는 지금 로그인된 사용자의 모든 채팅 제목과 생성 시간을 한번에 보여주는 메서드이다.

모든 채팅 기록중 어떤 채팅을 클릭하면 chat_id를 기반으로 getAllChatMessages() 메서드가 실행된다.

메서드가 실행되면 해당 채팅의 모든 기록이 프런트로 전달된다.

프론트는 전달된 데이터들을 이용해 쉽게 채팅기록을 화면에 보여줄 수 있다.

4. 채팅 삭제

public ResponseEntity<?> deleteChatRoom(Long chatId, HttpSession session) {
    // 채팅방을 찾습니다.
    Optional<ChatRoom> optionalChat = chatRoomRepository.findById(chatId);
    if (!optionalChat.isPresent()) {
        // 채팅방이 존재하지 않는 경우
        return ResponseEntity.notFound().build();
    }
    ChatRoom chatRoom = optionalChat.get();

    // 채팅방의 소유자가 아닌 경우
    if (!Objects.equals(chatRoom.getUser().getId(), getUserFromSession(session).getId())) {
        return ResponseEntity.status(HttpStatus.FORBIDDEN).body("삭제할 권한이 없습니다.");
    }

    // 채팅방 삭제
    chatRoomRepository.delete(chatRoom);

    return ResponseEntity.ok("채팅방이 성공적으로 삭제되었습니다.");
}

레시피 CRUD 기능 구현 (tistory.com)에서 소개한 삭제 기능과 전반적으로 비슷하다.

 

이렇게 채팅 기능이 1차적으로 완성되었다.

update가 없는 이유는 채팅 기록을 수정하는 것은 불필요하고,

사용자와 관리자의 충돌을 야기하는 기능이 될 것 같았기 때문이다.

글을 작성하다 보니 프론트의 필요에 따라 deleteAll() 기능을 만들어 놓는것도 좋을 듯 하다.