유저 간 대여를 위해선 의사소통이 필요합니다. 물론 게시글내의 존재하는 댓글 시스템을 이용할 수도 있겠지만, 좀 더 긴밀한 대화가 필요한 경우가 있겠죠. 그래서 채팅창을 만들어 유저 간의 대화를 용이하게 하도록 해보겠습니다.
우선 채팅창 디자인을 만들고, 이를 바탕으로 유저 인터페이스를 구현한 다음에 이전에 만든 message-service를 수정하도록 하겠습니다.
대략적으로 만들어본 UI 템플릿입니다. 이 템플릿을 기반으로 UI를 구현해보도록 하겠습니다.
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;
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;
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;
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;
여기까지가 좌측의 채팅목록 부분입니다. 이어서 우측의 채팅창을 만들어 보도록 하겠습니다.
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;
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;
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;
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를 만들어 확인을 하도록 하겠습니다.
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;
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를 수정하도록 하겠습니다.
UI를 기준으로 좌측엔 메시지목록, 즉 유저리스트가 있고 우측엔 채팅창이 존재합니다. 그러면 유저리스트를 가져오기 위한 REST, 채팅리스트를 가져오기 위한 REST를 만들어보도록 하겠습니다.
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: 채팅기록을 가져오는 메서드입니다.
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;
}
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;
}
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;
}
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;
}
}
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;
}
}
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 클래스를 작성하겠습니다.
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);
}
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를 위한 쿼리문을 작성하도록 하겠습니다.
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를 구현해보았고 다음 포스트에서는 프론트엔드와 연결을 해보도록 하겠습니다.