Rental Application (React & Spring boot Microservice) - 33 : 채팅(1)

yellow_note·2021년 9월 28일
0

#1 채팅창

유저 간 대여를 위해선 의사소통이 필요합니다. 물론 게시글내의 존재하는 댓글 시스템을 이용할 수도 있겠지만, 좀 더 긴밀한 대화가 필요한 경우가 있겠죠. 그래서 채팅창을 만들어 유저 간의 대화를 용이하게 하도록 해보겠습니다.

우선 채팅창 디자인을 만들고, 이를 바탕으로 유저 인터페이스를 구현한 다음에 이전에 만든 message-service를 수정하도록 하겠습니다.

대략적으로 만들어본 UI 템플릿입니다. 이 템플릿을 기반으로 UI를 구현해보도록 하겠습니다.

  • ./src/components/message/common/InputButton.js
import React from "react";
import styled from "styled-components";
import palette from "../../../lib/styles/palettes";

const SearchButtonArea = styled.div`
    width: 60px;
    float: left;
`;

const Button = styled.button`
    width: 90px;
    height: 40px;
    border-radius: 4px;
    background-color: ${ palette.blue[1] };
    color: #ffffff;
    outline: none;
    border: none;
    &: hover {
        width: 90px;
        height: 40px;
        border-radius: 4px;
        background-color: ${ palette.blue[2] };
        color: #ffffff;
        outline: none;
        border: none;
    }
`;

const SearchButton = () =>{
    return(
        <SearchButtonArea>
            <Button>
                검색
            </Button>
        </SearchButtonArea>
    );
};

export default SearchButton;
  • ./src/components/message/common/InputBar.js
import React from 'react';
import styled from 'styled-components';
import Input from '../../common/Input';

const SearchBarArea = styled.div`
    float: left;
    width: inherit;
`;

const SearchBar = ({ placeholder }) => {
    return(
        <SearchBarArea>
            <Input name="searchBar"
                   type="text"
                   placeholder={ placeholder }
            />
        </SearchBarArea>
    );
};

export default SearchBar;
  • ./src/components/message/MessageCard.js
import React from 'react';
import styled from 'styled-components';
import palette from '../../lib/styles/palettes';

const Card = styled.div`
    width: 100%;
    height: 80px;
    &:hover {
        background-color: ${palette.gray[2]}
    }
    padding-top: 1rem;
    padding-left: 4rem;
`;

const Nickname = styled.div`
    float: left;
    width: 70%;
    height: 40px;
`;

const Date = styled.div`
    float: left;
    width: 30%;
    height: 40px;
`;

const Content = styled.div`
    width: 100%;
    height: 80px;
    overflow: hidden;
`;

const MessageCard = ({ item, i }) => {
    return(
        <Card>
            <Nickname>
                { item.nickname } 
            </Nickname>
            <Date>
                { item.createdAt }
            </Date>
            <Content>
                { item.content }
            </Content>
        </Card>
    );
};

export default MessageCard;
  • ./src/components/message/MessageListContainer.js
import React from 'react';
import styled from 'styled-components';
import SearchBar from './common/InputBar';
import SearchButton from './common/InpuButton';
import MessageCard from './MessageCard';

const Wrap = styled.div`
    float: left;
    padding-top: 150px;
    width: 30%;
    height: 600px;
    overflow-x: hidden;
    overflow-y: auto;
`;

const Header  = styled.div`
    width: 80%;
    display: flex;
    align-items: center;
    justify-content: center;
    padding-left: 1.5rem;
`;

const ListBox = styled.div`
    display: flex;
    justify-content: center;
    align-items: center;
    flex-direction: column;
    padding-top: 20px;
    width: 100%;
`;

const MessageListContainer = () => {
    const dummyData = [
        { "nickname": "test-01", "createdAt": "2020-01-01", "content": "test-01" },
        { "nickname": "test-02", "createdAt": "2020-01-01", "content": "test-02" },
        { "nickname": "test-03", "createdAt": "2020-01-01", "content": "test-03" },
        { "nickname": "test-04", "createdAt": "2020-01-01", "content": "test-04" },
        { "nickname": "test-05", "createdAt": "2020-01-01", "content": "test-05" },
        { "nickname": "test-06", "createdAt": "2020-01-01", "content": "test-06" },
        { "nickname": "test-07", "createdAt": "2020-01-01", "content": "test-07" },
        { "nickname": "test-08", "createdAt": "2020-01-01", "content": "test-08" },
    ];

    return(
        <Wrap>
            <Header>
                <SearchBar placeholder="닉네임을 입력해주세요" />
                <SearchButton />            
            </Header>
            <ListBox>
                {
                    dummyData.map((item, i) => {
                        return <MessageCard item={ item } 
                                            i={ i }
                               />
                    })
                }
            </ListBox>
        </Wrap>
    );
};

export default MessageListContainer;

여기까지가 좌측의 채팅목록 부분입니다. 이어서 우측의 채팅창을 만들어 보도록 하겠습니다.

  • ./src/components/message/SendForm.js
import React from 'react';
import styled from 'styled-components';
import InputBar from './common/InputBar';
import InputButton from './common/InpuButton';

const SendContainer = styled.div`
    width: inherit;
    display: flex;
    align-items: center;
    justify-content: center;
    position: fixed;
    overflow-y: auto;
    overflow-x: hidden;
`;

const StyledInputBar = styled(InputBar)`
    width: 100%;
`;

const SendForm = () => {
    return (
        <SendContainer>
            <StyledInputBar placeholder="메시지를 입력해주세요" />
            <InputButton />            
        </SendContainer>
    );
};

export default SendForm;
  • ./src/components/message/ChatCard.js
import React from 'react';
import styled from 'styled-components';
import palette from '../../lib/styles/palettes';

const Card = styled.div`
    width: 100%;
`;

const SenderDate = styled.span`
    float: left;
    width: 98.5%;
    text-align: right;
`;

const ReceiverDate = styled.span`
    float: left;
    width: 100%;
    padding-left: 15px;
`;

const SenderCard = styled.div`
    float: right;
    display: inline-block;
    padding: 20px;
    margin: 10px;
    background-color: ${palette.blue[2]};
    color: white;
    border-radius: 10px;
`;

const ReceiverCard = styled.div`
    float: left;
    display: inline-block;
    padding: 20px;
    margin: 10px;
    background-color: ${palette.gray[2]};
    border-radius: 10px;
`;

const ChatCard = () => {
    return(
        <>
            <Card>
                <ReceiverCard>
                    asdsadsadsa
                </ReceiverCard>
                <ReceiverDate>
                    2020-01-01
                </ReceiverDate>
            </Card>
            <Card>
                <SenderCard>
                    asdsadsad
                </SenderCard>
                <SenderDate>
                    2020-01-02
                </SenderDate>
            </Card>
        </>
    );
};

export default ChatCard;
  • ./src/components/message/RoomTemplate.js
import React from 'react';
import styled from 'styled-components';
import ChatCard from './ChatCard';

const Wrap = styled.div`
    width: 100%;
    height: 560px;
`;

const RoomTemplate = () => {
    return(
        <Wrap>
            <ChatCard />
        </Wrap>
    );
};

export default RoomTemplate;
  • ./src/components/message/RoomContainer.js
import React from 'react';
import styled from 'styled-components';
import RoomTemplate from './RoomTemplate';
import SendForm from './SendForm';

const Wrap = styled.div`
    padding-top: 150px;
    width: 70%;
    height: 600px;
    overflow-x: hidden;
    overflow-y: auto;
`;

const RoomContainer = () => {
    return(
        <Wrap>
            <RoomTemplate />
            <SendForm />
        </Wrap>
    );
};

export default RoomContainer;

우측의 채팅창까지 구현이 완료되었으니 MessagePage를 만들어 확인을 하도록 하겠습니다.

  • ./src/pages/MessagePage.js
import React from 'react';
import HeaderTemplate from '../components/common/HeaderTemplate';
import MessageListContainer from '../components/message/MessageListContainer';
import RoomContainer from '../components/message/RoomContainer';

const MessagePage = () => {
    return(
        <>
            <HeaderTemplate />
            <MessageListContainer />
            <RoomContainer />
        </>
    );
};

export default MessagePage;
  • ./src/App.js
import React from 'react';
import { Route } from 'react-router-dom';
import HomePage from './pages/HomePage';
import LoginPage from './pages/LoginPage';
import MessagePage from './pages/MessagePage';
import MyPage from './pages/MyPage';
import PostDetailPage from './pages/PostDetailPage';
import PostPage from './pages/PostPage';
import RegisterPage from './pages/RegisterPage';
import WritePage from './pages/WritePage';

const App = () => {
    return(
        <>
            ...
            <Route 
                component={ MessagePage }
                path="/messages"
                exact
            />
        </>
    );
};

export default App;

페이지가 잘 나오는 모습을 확인할 수 있습니다. 그러면 이어서 message-service를 수정하도록 하겠습니다.

#2 message-service

UI를 기준으로 좌측엔 메시지목록, 즉 유저리스트가 있고 우측엔 채팅창이 존재합니다. 그러면 유저리스트를 가져오기 위한 REST, 채팅리스트를 가져오기 위한 REST를 만들어보도록 하겠습니다.

  • ./controller/MessageController
package com.microservices.messageservice.controller;

import com.microservices.messageservice.dto.MessageDto;
import com.microservices.messageservice.service.MessageService;
import com.microservices.messageservice.vo.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.ArrayList;
import java.util.List;

@RestController
@RequestMapping("/")
@Slf4j
public class MessageController {
    private MessageService messageService;
    private Environment env;

    @Autowired
    public MessageController(
        MessageService messageService,
        Environment env
    ) {
        this.messageService = messageService;
        this.env = env;
    }

    @GetMapping("/health_check")
    public String status() {
        return String.format(
            "It's working in Post Service"
                + ", port(local.server.port) =" + env.getProperty("local.server.port")
                + ", port(server.port) =" + env.getProperty("server.port")
        );
    }

    @PostMapping("/send")
    public ResponseEntity<?> send(@RequestBody RequestSend vo) {
        log.info("Message Service's Controller Layer :: Call send Method!");

        MessageDto messageDto = MessageDto.builder()
                                          .sender(vo.getSender())
                                          .receiver(vo.getReceiver())
                                          .content(vo.getContent())
                                          .build();

        return ResponseEntity.status(HttpStatus.CREATED).body("Successfully send message to " + messageService.send(messageDto).getReceiver());
    }

    @GetMapping("/user-list/{sender}")
    public ResponseEntity<?> getUserList(@PathVariable("sender") String sender) {
        log.info("Message Service's Controller Layer :: Call getUserList Method!");

        Iterable<MessageDto> messageList = messageService.getUserList(sender);
        List<ResponseMessage> result = new ArrayList<>();

        messageList.forEach(v -> {
            result.add(ResponseMessage.builder()
                                      .sender(v.getSender())
                                      .receiver(v.getReceiver())
                                      .build()
            );
        });

        return ResponseEntity.status(HttpStatus.OK).body(result);
    }

    @GetMapping("/message-list")
    public ResponseEntity<?> getMessageList(
        @RequestParam("receiver") String receiver,
        @RequestParam("sender") String sender
    ) {
        log.info("Message Service's Controller Layer :: Call getMessageList Method!");

        Iterable<MessageDto> messageList = messageService.getMessageList(sender,
                                                                         receiver);
        List<ResponseMessage> result = new ArrayList<>();

        messageList.forEach(v -> {
            result.add(ResponseMessage.builder()
                                      .id(v.getId())
                                      .sender(v.getSender())
                                      .receiver(v.getReceiver())
                                      .content(v.getContent())
                                      .createdAt(v.getCreatedAt())
                                      .build()
            );
        });

        return ResponseEntity.status(HttpStatus.OK).body(result);
    }

    @DeleteMapping("/")
    public ResponseEntity<?> deleteMessageList(@RequestBody RequestDelete vo) {
        log.info("Message Service's Controller Layer :: Call deleteMessageList Method!");

        String message = messageService.delete(vo.getSender(),
                                               vo.getReceiver());

        return ResponseEntity.status(HttpStatus.OK).body(message);
    }
}

1) POST /message-service/send : 메시지 저장을 위한 메서드입니다.
2) GET /message-service/user-list: 유저 리스트를 불러오는 메서드입니다.
3) GET /message-service/message-list: 채팅기록을 가져오는 메서드입니다.

  • ./vo/RequestSend
package com.microservices.messageservice.vo;

import lombok.Getter;

import javax.validation.constraints.NotNull;

@Getter
public class RequestSend {
    @NotNull(message="Sender cannot be null")
    private String sender;

    @NotNull(message="Receiver cannot be null")
    private String receiver;

    @NotNull(message="Content cannot be null")
    private String content;
}
  • ./vo/RequestMessageList
package com.microservices.messageservice.vo;

import lombok.Getter;

import javax.validation.constraints.NotNull;

@Getter
public class RequestMessageList {
    @NotNull(message="Sender cannot be null")
    private String sender;

    @NotNull(message="Receiver cannot be null")
    private String receiver;
}
  • ./vo/RequestUserList
package com.microservices.messageservice.vo;

import lombok.Getter;

import javax.validation.constraints.NotNull;

@Getter
public class RequestUserList {
    @NotNull(message="Receiver cannot be null")
    private String receiver;
}
  • ./vo/ResponseMessage
package com.microservices.messageservice.vo;

import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Builder;
import lombok.Data;

import java.time.LocalDateTime;

@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ResponseMessage {
    private Long id;
    private String sender;
    private String receiver;
    private String content;
    private LocalDateTime createdAt;

    @Builder
    public ResponseMessage(
        Long id,
        String sender,
        String receiver,
        String content,
        LocalDateTime createdAt
    ) {
        this.id = id;
        this.sender = sender;
        this.receiver = receiver;
        this.content = content;
        this.createdAt = createdAt;
    }
}
  • ./dto/MessageDto
package com.microservices.messageservice.dto;

import lombok.Builder;
import lombok.Data;

import java.time.LocalDateTime;

@Data
public class MessageDto {
    private Long id;
    private String sender;
    private String receiver;
    private String content;
    private LocalDateTime createdAt;

    @Builder
    public MessageDto(
        Long id,
        String sender,
        String receiver,
        String content,
        LocalDateTime createdAt
    ) {
        this.id = id;
        this.sender = sender;
        this.receiver = receiver;
        this.content = content;
        this.createdAt = createdAt;
    }
}
  • ./entity/MessageEntity
package com.microservices.messageservice.entity;

import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.CreatedDate;

import javax.persistence.*;
import java.time.LocalDateTime;

@Data
@Entity
@Table(name="messages")
@NoArgsConstructor
public class MessageEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String sender;

    @Column(nullable = false)
    private String receiver;

    @Column(nullable = false)
    private String content;

    @CreatedDate
    @Column(nullable = false)
    private LocalDateTime createdAt;

    @Builder
    public MessageEntity(
        Long id,
        String sender,
        String receiver,
        String content,
        LocalDateTime createdAt
    ) {
        this.id = id;
        this.sender = sender;
        this.receiver = receiver;
        this.content = content;
        this.createdAt = createdAt;
    }
}

vo, dto, entity 클래스를 작성했으니 컨트롤러에서의 남은 오류 코드는 Service만 남았습니다. Service 클래스를 작성하겠습니다.

  • ./service/MessageService
package com.microservices.messageservice.service;

import com.microservices.messageservice.dto.MessageDto;

public interface MessageService {
    MessageDto send(MessageDto dto);

    Iterable<MessageDto> getUserList(String sender);

    Iterable<MessageDto> getMessageList(String sender,
                                        String receiver);

    String delete(String sender,
                  String receiver);
}
  • ./service/MessageServiceImpl
package com.microservices.messageservice.service;

import com.microservices.messageservice.dto.MessageDto;
import com.microservices.messageservice.entity.MessageEntity;
import com.microservices.messageservice.repository.MessageRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import javax.transaction.Transactional;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

@Service
@Slf4j
public class MessageServiceImpl implements MessageService {
    private MessageRepository messageRepository;

    @Autowired
    public MessageServiceImpl(MessageRepository messageRepository) {
        this.messageRepository = messageRepository;
    }

    @Transactional
    @Override
    public MessageDto send(MessageDto dto) {
        log.info("Message Service's Service Layer :: Call send Method!");

        MessageEntity messageEntity = MessageEntity.builder()
                                                   .sender(dto.getSender())
                                                   .receiver(dto.getReceiver())
                                                   .content(dto.getContent())
                                                   .createdAt(LocalDateTime.now())
                                                   .build();

        messageRepository.save(messageEntity);

        return MessageDto.builder()
                         .sender(dto.getSender())
                         .receiver(dto.getReceiver())
                         .content(dto.getContent())
                         .build();
    }

    @Transactional
    @Override
    public Iterable<MessageDto> getUserList(String sender) {
        log.info("Message Service's Service Layer :: Call getUserList Method!");

        Iterable<Object[]> messageList = messageRepository.findUserList(sender);
        List<MessageDto> messages = new ArrayList<>();

        messageList.forEach(message -> {
            messages.add(MessageDto.builder()
                                   .sender(String.valueOf(message[0]))
                                   .receiver(String.valueOf(message[1]))
                                   .build());
        });

        return messages;
    }

    @Transactional
    @Override
    public Iterable<MessageDto> getMessageList(
        String sender,
        String receiver
    ) {
        log.info("Message Service's Service Layer :: Call getMessageList Method!");

        Iterable<MessageEntity> messageList = messageRepository.findMessageList(sender,
                                                                               receiver);
        List<MessageDto> messages = new ArrayList<>();

        messageList.forEach(message -> {
            messages.add(MessageDto.builder()
                                   .id(message.getId())
                                   .sender(message.getSender())
                                   .receiver(message.getReceiver())
                                   .content(message.getContent())
                                   .createdAt(message.getCreatedAt())
                                   .build());
        });

        return messages;
    }

    @Transactional
    @Override
    public String delete(
        String sender,
        String receiver
    ) {
        log.info("Message Service's Service Layer :: Call delete Method!");

        messageRepository.deleteBySenderAndReceiver(sender,
                                                    receiver);

        return "Successfully Delete messages";
    }
}

Service클래스에서는 복잡한 로직이 없으니 Repository에서 데이터 CRUD를 위한 쿼리문을 작성하도록 하겠습니다.

  • ./repository/MessageRepository
package com.microservices.messageservice.repository;

import com.microservices.messageservice.entity.MessageEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;

@Repository
public interface MessageRepository extends JpaRepository<MessageEntity, Long> {
    @Query(value = "SELECT distinct sender, receiver " +
                   "FROM messages m " +
                   "WHERE m.sender = :sender OR m.receiver = :sender " +
                   "ORDER BY m.id DESC",
           nativeQuery = true)
    Iterable<Object[]> findUserList(String sender);

    @Query(value = "SELECT * " +
                   "FROM messages m " +
                   "WHERE (m.receiver = :receiver AND m.sender = :sender) OR (m.receiver = :sender AND m.sender = :receiver)" +
                   "ORDER BY m.id ASC",
           nativeQuery = true)
    Iterable<MessageEntity> findMessageList(
        String sender,
        String receiver
    );

    @Query(
        value = "DELETE " +
                "FROM messages m " +
                "WHERE m.receiver = :receiver AND m.sender = :sender",
        nativeQuery = true
    )
    void deleteBySenderAndReceiver(
        String receiver,
        String sender
    );
}

1) findUserList: 내림차순으로 메시지 데이터를 가져오는 쿼리문을 가지고 있습니다. distinct를 이용하여 중복을 없애고 유저, 가장 최근의 메시지내역, 수신날짜를 가져와 하나의 카드를 만드는 트랜잭션입니다. 여기서 receiver는 현재 로그인한 유저의 닉네임입니다.

2) findMessageList: 오름차순으로 메시지 데이터를 가져오는 쿼리문입니다. sender, receiver를 조건으로 넣어 두 명의 유저 간 메시지리스트를 가져오는 트랜잭션입니다.

여기까지 message-service를 구현해보았고 다음 포스트에서는 프론트엔드와 연결을 해보도록 하겠습니다.

0개의 댓글