diff --git a/build.gradle b/build.gradle index a497710e..77aa6b4d 100644 --- a/build.gradle +++ b/build.gradle @@ -84,6 +84,10 @@ dependencies { // gson implementation 'com.google.code.gson:gson:2.9.0' + // dynamodb + implementation 'com.amazonaws:aws-java-sdk-s3:1.12.268' + implementation 'io.github.boostchicken:spring-data-dynamodb:5.2.5' + // test testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.security:spring-security-test' diff --git a/lombok.config b/lombok.config index 731b780d..bf221d0a 100644 --- a/lombok.config +++ b/lombok.config @@ -1,5 +1,4 @@ lombok.copyableAnnotations += org.springframework.beans.factory.annotation.Value lombok.copyableannotations += com.dku.council.global.config.webclient.ChromeAgentWebClient lombok.anyConstructor.addConstructorProperties = true -lombok.Setter.flagUsage = error lombok.data.flagUsage = error \ No newline at end of file diff --git a/src/main/java/com/dku/council/domain/chat/controller/ChatController.java b/src/main/java/com/dku/council/domain/chat/controller/ChatController.java index 37d0c522..b1b0308f 100644 --- a/src/main/java/com/dku/council/domain/chat/controller/ChatController.java +++ b/src/main/java/com/dku/council/domain/chat/controller/ChatController.java @@ -6,6 +6,9 @@ import com.dku.council.domain.chat.model.dto.response.ResponseChatDto; import com.dku.council.domain.chat.service.ChatService; import com.dku.council.domain.chat.service.MessageSender; +import com.dku.council.domain.chatmessage.model.entity.ChatRoomMessage; +import com.dku.council.domain.chatmessage.service.ChatRoomMessageService; +import com.dku.council.domain.user.service.UserService; import com.dku.council.global.auth.role.UserAuth; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; @@ -18,7 +21,9 @@ import org.springframework.messaging.simp.SimpMessageSendingOperations; import org.springframework.messaging.simp.stomp.StompHeaderAccessor; import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.socket.messaging.SessionDisconnectEvent; @@ -39,7 +44,9 @@ public class ChatController { */ private final SimpMessageSendingOperations template; + private final UserService userService; private final ChatService chatService; + private final ChatRoomMessageService chatRoomMessageService; private final MessageSender sender; /** @@ -61,16 +68,22 @@ public void enterUser(@Payload RequestChatDto chat, String username = chatService.addUser(chat.getRoomId(), chat.getSender()); log.info("enterUser에서 uuid " + username); log.info("enterUser에서 roomId " + chat.getRoomId()); + log.info("enterUser에서 userId " + chat.getUserId()); // 반환 결과를 socket session 에 userUUID 로 저장 headerAccessor.getSessionAttributes().put("username", username); + headerAccessor.getSessionAttributes().put("userId", chat.getUserId()); headerAccessor.getSessionAttributes().put("roomId", chat.getRoomId()); + String enterMessage = chat.getSender() + " 님 입장!!"; + // 입장 메시지 저장 + chatRoomMessageService.create(chat.getRoomId(), chat.getType().toString(), chat.getUserId(), chat.getSender(), enterMessage); + Message message = Message.builder() .type(chat.getType()) .roomId(chat.getRoomId()) .sender(chat.getSender()) - .message(chat.getSender() + " 님 입장!!") + .message(enterMessage) .build(); sender.send(topic, message); @@ -85,6 +98,8 @@ public void enterUser(@Payload RequestChatDto chat, public void sendMessage(@Payload RequestChatDto chat) { log.info("CHAT {}", chat); + chatRoomMessageService.create(chat.getRoomId(), chat.getType().toString(), chat.getUserId(), chat.getSender(), chat.getMessage()); + Message message = Message.builder() .type(chat.getType()) .roomId(chat.getRoomId()) @@ -108,6 +123,7 @@ public void webSocketDisconnectListener(SessionDisconnectEvent event) { // stomp 세션에 있던 username과 roomId 를 확인해서 채팅방 유저 리스트와 room 에서 해당 유저를 삭제 String username = (String) headerAccessor.getSessionAttributes().get("username"); + Long userId = (Long) headerAccessor.getSessionAttributes().get("userId"); String roomId = (String) headerAccessor.getSessionAttributes().get("roomId"); log.info("퇴장 controller에서 uuid " + username); log.info("퇴장 controller에서 roomId " + roomId); @@ -123,12 +139,15 @@ public void webSocketDisconnectListener(SessionDisconnectEvent event) { if (username != null) { log.info("User Disconnected : ", username); + String exitMessage = username + " 님 퇴장!!"; + // 퇴장 메시지 저장 + chatRoomMessageService.create(roomId, MessageType.LEAVE.toString(), userId, username, exitMessage); // builder 어노테이션 활용 Message message = Message.builder() .type(MessageType.LEAVE) .sender(username) .roomId(roomId) - .message(username + " 님 퇴장!!") + .message(exitMessage) .build(); sender.send(topic, message); @@ -146,4 +165,12 @@ public void webSocketDisconnectListener(SessionDisconnectEvent event) { public List userList(String roomId) { return chatService.getUserList(roomId); } + + + @GetMapping("/chat/message/list") + @UserAuth + @ResponseBody + public List list(@RequestParam("roomId") String roomId) { + return chatRoomMessageService.findAllChatRoomMessages(roomId); + } } \ No newline at end of file diff --git a/src/main/java/com/dku/council/domain/chat/controller/ChatRoomController.java b/src/main/java/com/dku/council/domain/chat/controller/ChatRoomController.java index f982f607..c46901a8 100644 --- a/src/main/java/com/dku/council/domain/chat/controller/ChatRoomController.java +++ b/src/main/java/com/dku/council/domain/chat/controller/ChatRoomController.java @@ -2,6 +2,7 @@ import com.dku.council.domain.chat.model.dto.response.ResponseChatRoomDto; import com.dku.council.domain.chat.service.ChatService; +import com.dku.council.domain.chatmessage.service.ChatRoomMessageService; import com.dku.council.domain.user.model.dto.response.ResponseUserInfoForChattingDto; import com.dku.council.domain.user.service.UserService; import com.dku.council.global.auth.jwt.AppAuthentication; @@ -24,6 +25,7 @@ public class ChatRoomController { private final ChatService chatService; private final UserService userService; + private final ChatRoomMessageService chatRoomMessageService; /** * 채팅방 리스트 화면 @@ -111,6 +113,8 @@ public boolean confirmPwd(@PathVariable String roomId, @UserAuth public String delChatRoom(@PathVariable String roomId, AppAuthentication auth){ + chatRoomMessageService.deleteChatRoomMessages(roomId); + // roomId(UUID 값) 기준으로 채팅방 삭제 chatService.delChatRoom(auth.getUserId(), roomId, auth.isAdmin()); diff --git a/src/main/java/com/dku/council/domain/chat/model/dto/request/RequestChatDto.java b/src/main/java/com/dku/council/domain/chat/model/dto/request/RequestChatDto.java index 9a753724..641e3998 100644 --- a/src/main/java/com/dku/council/domain/chat/model/dto/request/RequestChatDto.java +++ b/src/main/java/com/dku/council/domain/chat/model/dto/request/RequestChatDto.java @@ -12,6 +12,8 @@ public class RequestChatDto { private final String roomId; + private final Long userId; + private final String sender; private final String message; diff --git a/src/main/java/com/dku/council/domain/chatmessage/model/ChatRoomMessageId.java b/src/main/java/com/dku/council/domain/chatmessage/model/ChatRoomMessageId.java new file mode 100644 index 00000000..63c08283 --- /dev/null +++ b/src/main/java/com/dku/council/domain/chatmessage/model/ChatRoomMessageId.java @@ -0,0 +1,24 @@ +package com.dku.council.domain.chatmessage.model; + +import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBHashKey; +import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBRangeKey; +import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBTypeConverted; +import com.dku.council.global.config.DynamoDBConfig; +import lombok.Getter; +import lombok.Setter; + +import java.io.Serializable; +import java.time.LocalDateTime; + +@Getter +@Setter +public class ChatRoomMessageId implements Serializable { + private static final long serialVersionUID = 1L; + + @DynamoDBHashKey + private String roomId; + + @DynamoDBRangeKey + @DynamoDBTypeConverted(converter = DynamoDBConfig.LocalDateTimeConverter.class) + private LocalDateTime createdAt; +} diff --git a/src/main/java/com/dku/council/domain/chatmessage/model/entity/ChatRoomMessage.java b/src/main/java/com/dku/council/domain/chatmessage/model/entity/ChatRoomMessage.java new file mode 100644 index 00000000..e6f18d2c --- /dev/null +++ b/src/main/java/com/dku/council/domain/chatmessage/model/entity/ChatRoomMessage.java @@ -0,0 +1,61 @@ +package com.dku.council.domain.chatmessage.model.entity; + +import com.amazonaws.services.dynamodbv2.datamodeling.*; +import com.dku.council.domain.chatmessage.model.ChatRoomMessageId; +import com.dku.council.global.config.DynamoDBConfig; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.springframework.data.annotation.Id; + +import java.time.LocalDateTime; +@DynamoDBTable(tableName = "ChatRoomMessage") +@Getter +@Setter +@NoArgsConstructor() +public class ChatRoomMessage { + + @Id + @Getter(AccessLevel.NONE) + @Setter(AccessLevel.NONE) + private ChatRoomMessageId chatRoomMessageId; + + @DynamoDBHashKey(attributeName = "roomId") + public String getRoomId() { + return chatRoomMessageId != null ? chatRoomMessageId.getRoomId() : null; + } + + public void setRoomId(String roomId) { + if (chatRoomMessageId == null) { + chatRoomMessageId = new ChatRoomMessageId(); + } + chatRoomMessageId.setRoomId(roomId); + } + + @DynamoDBRangeKey(attributeName = "createdAt") + @DynamoDBTypeConverted(converter = DynamoDBConfig.LocalDateTimeConverter.class) + public LocalDateTime getCreatedAt() { + return chatRoomMessageId != null ? chatRoomMessageId.getCreatedAt() : null; + } + + @DynamoDBTypeConverted(converter = DynamoDBConfig.LocalDateTimeConverter.class) + public void setCreatedAt(LocalDateTime createdAt) { + if (chatRoomMessageId == null) { + chatRoomMessageId = new ChatRoomMessageId(); + } + chatRoomMessageId.setCreatedAt(createdAt); + } + + @DynamoDBAttribute + private String messageType; + + @DynamoDBAttribute + private Long userId; + + @DynamoDBAttribute + private String userNickname; + + @DynamoDBAttribute + private String content; +} \ No newline at end of file diff --git a/src/main/java/com/dku/council/domain/chatmessage/repository/ChatRoomMessageRepository.java b/src/main/java/com/dku/council/domain/chatmessage/repository/ChatRoomMessageRepository.java new file mode 100644 index 00000000..17d253ef --- /dev/null +++ b/src/main/java/com/dku/council/domain/chatmessage/repository/ChatRoomMessageRepository.java @@ -0,0 +1,17 @@ +package com.dku.council.domain.chatmessage.repository; + +import com.dku.council.domain.chatmessage.model.ChatRoomMessageId; +import com.dku.council.domain.chatmessage.model.entity.ChatRoomMessage; +import org.socialsignin.spring.data.dynamodb.repository.EnableScan; +import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@EnableScan +@Repository +public interface ChatRoomMessageRepository extends CrudRepository { + List findAllByRoomIdOrderByCreatedAtAsc(String roomId); + + void deleteAllByRoomId(String roomId); +} diff --git a/src/main/java/com/dku/council/domain/chatmessage/service/ChatRoomMessageService.java b/src/main/java/com/dku/council/domain/chatmessage/service/ChatRoomMessageService.java new file mode 100644 index 00000000..ceceb31f --- /dev/null +++ b/src/main/java/com/dku/council/domain/chatmessage/service/ChatRoomMessageService.java @@ -0,0 +1,46 @@ +package com.dku.council.domain.chatmessage.service; + +import com.dku.council.domain.chatmessage.model.entity.ChatRoomMessage; +import com.dku.council.domain.chatmessage.repository.ChatRoomMessageRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.List; + +@Service +@RequiredArgsConstructor +@Slf4j +public class ChatRoomMessageService { + + private final ZoneId seoulZoneId = ZoneId.of("Asia/Seoul"); + + private final ChatRoomMessageRepository chatRoomMessageRepository; + + public void create(String roomId, + String messageType, + Long userId, + String userNickname, + String content) { + + ChatRoomMessage chatRoomMessage = new ChatRoomMessage(); + chatRoomMessage.setRoomId(roomId); + chatRoomMessage.setMessageType(messageType); + chatRoomMessage.setUserId(userId); + chatRoomMessage.setUserNickname(userNickname); + chatRoomMessage.setContent(content); + chatRoomMessage.setCreatedAt(LocalDateTime.now().atZone(seoulZoneId).toLocalDateTime()); + + chatRoomMessageRepository.save(chatRoomMessage); + } + + public List findAllChatRoomMessages(String roomId) { + return chatRoomMessageRepository.findAllByRoomIdOrderByCreatedAtAsc(roomId); + } + + public void deleteChatRoomMessages(String roomId) { + chatRoomMessageRepository.deleteAllByRoomId(roomId); + } +} diff --git a/src/main/java/com/dku/council/domain/user/model/dto/response/ResponseUserInfoForChattingDto.java b/src/main/java/com/dku/council/domain/user/model/dto/response/ResponseUserInfoForChattingDto.java index e3100098..0e24932d 100644 --- a/src/main/java/com/dku/council/domain/user/model/dto/response/ResponseUserInfoForChattingDto.java +++ b/src/main/java/com/dku/council/domain/user/model/dto/response/ResponseUserInfoForChattingDto.java @@ -6,8 +6,7 @@ @Getter @RequiredArgsConstructor public class ResponseUserInfoForChattingDto { - private final String studentId; - private final String username; + private final Long userId; private final String nickname; private final boolean isAdmin; } diff --git a/src/main/java/com/dku/council/domain/user/service/UserService.java b/src/main/java/com/dku/council/domain/user/service/UserService.java index 8eba9cde..7e42bdd2 100644 --- a/src/main/java/com/dku/council/domain/user/service/UserService.java +++ b/src/main/java/com/dku/council/domain/user/service/UserService.java @@ -120,6 +120,6 @@ public void isDkuChecked(Long userId) { public ResponseUserInfoForChattingDto getUserInfoForChatting(Long memberId) { User user = userRepository.findById(memberId).orElseThrow(UserNotFoundException::new); - return new ResponseUserInfoForChattingDto(user.getStudentId(), user.getName(), user.getNickname(), user.getUserRole().isAdmin()); + return new ResponseUserInfoForChattingDto(user.getId(), user.getNickname(), user.getUserRole().isAdmin()); } } diff --git a/src/main/java/com/dku/council/global/config/DynamoDBConfig.java b/src/main/java/com/dku/council/global/config/DynamoDBConfig.java new file mode 100644 index 00000000..93a83f76 --- /dev/null +++ b/src/main/java/com/dku/council/global/config/DynamoDBConfig.java @@ -0,0 +1,67 @@ +package com.dku.council.global.config; + +import com.amazonaws.auth.AWSCredentials; +import com.amazonaws.auth.AWSCredentialsProvider; +import com.amazonaws.auth.AWSStaticCredentialsProvider; +import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.services.dynamodbv2.AmazonDynamoDB; +import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClientBuilder; +import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBTypeConverter; +import org.socialsignin.spring.data.dynamodb.repository.config.EnableDynamoDBRepositories; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.FilterType; +import org.springframework.stereotype.Repository; + +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.Date; + +@Configuration +@EnableDynamoDBRepositories(basePackages = {"com.dku.council.domain.chatmessage.repository"}, + includeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Repository.class)) +public class DynamoDBConfig { + + @Value("${aws.dynamodb.accessKey}") + private String awsAccessKey; + + @Value("${aws.dynamodb.secretKey}") + private String awsSecretKey; + + @Value("${aws.dynamodb.region}") + private String awsRegion; + + public AWSCredentials amazonAWSCredentials() { + return new BasicAWSCredentials(awsAccessKey, awsSecretKey); + } + + public AWSCredentialsProvider amazonAWSCredentialsProvider() { + return new AWSStaticCredentialsProvider(amazonAWSCredentials()); + } + + @Bean + public AmazonDynamoDB amazonDynamoDB() { + return AmazonDynamoDBClientBuilder.standard().withCredentials(amazonAWSCredentialsProvider()) + .withRegion(awsRegion).build(); + } + + /** + * Java DynamoDB SDK가 Java의 기본 Date 타입만 허용하므로 + * LocalDateTimeType과 Date를 상호 변환할 수 있는 컨버터 추가 + */ + public static class LocalDateTimeConverter implements DynamoDBTypeConverter { + @Override + public Date convert(LocalDateTime source) { + ZoneId seoulZoneId = ZoneId.of("Asia/Seoul"); + return Date.from(source.atZone(seoulZoneId).toInstant()); + } + + @Override + public LocalDateTime unconvert(Date source) { + return source.toInstant().atZone(ZoneId.of("Asia/Seoul")).toLocalDateTime(); + } + } + +} diff --git a/src/main/resources/static/js/chatroom/socket.js b/src/main/resources/static/js/chatroom/socket.js index 3bc44feb..2c3cd27c 100644 --- a/src/main/resources/static/js/chatroom/socket.js +++ b/src/main/resources/static/js/chatroom/socket.js @@ -15,6 +15,7 @@ var connectingElement = document.querySelector('.connecting'); var stompClient = null; var username = null; +var userId = null; var colors = [ '#2196F3', '#32c787', '#00BCD4', '#ff5652', @@ -27,6 +28,7 @@ const roomId = url.get('roomId'); function connect(event) { username = document.querySelector('#name').value.trim(); + userId = document.querySelector('#userId').value.trim(); // chatPage 를 등장시킴 chatPage.classList.remove('hidden'); @@ -47,6 +49,8 @@ function onConnected() { // sub 할 url => /sub/chat/room/roomId 로 구독한다 stompClient.subscribe('/sub/chatRoom/enter' + roomId, onMessageReceived); + getPreviousMessageList(); + // 서버에 username 을 가진 유저가 들어왔다는 것을 알림 // /pub/chat/enterUser 로 메시지를 보냄 console.log("확인" + username); @@ -54,6 +58,7 @@ function onConnected() { {}, JSON.stringify({ "roomId": roomId, + "userId": userId, sender: username, type: 'ENTER' }) @@ -85,6 +90,32 @@ function getUserList() { }) } +function getPreviousMessageList() { + + $.ajax({ + type: "GET", + url: "/chat/message/list", + data: { + "roomId": roomId + }, + success: function (data) { + console.log("이전 메시지들 확인" + data); + for (let i = 0; i < data.length; i++) { + let previousChatMessage = { + "roomId": data[i]["roomId"], + "userId": data[i]["userId"], + sender: data[i]["userNickname"], + message: data[i]["content"], + type: data[i]["messageType"], + createdAt: data[i]["createdAt"] + }; + console.log(previousChatMessage); + previousMessageReceived(JSON.stringify(previousChatMessage)); + } + } + }) +} + function onError(error) { connectingElement.textContent = 'Could not connect to WebSocket server. Please refresh this page to try again!'; @@ -98,6 +129,7 @@ function sendMessage(event) { if (messageContent && stompClient) { var chatMessage = { "roomId": roomId, + "userId": userId, sender: username, message: messageInput.value, type: 'TALK' @@ -109,51 +141,55 @@ function sendMessage(event) { event.preventDefault(); } -// 메시지를 받을 때도 마찬가지로 JSON 타입으로 받으며, +// API를 통해 메시지를 받을 때도 마찬가지로 JSON 타입으로 받으며, // 넘어온 JSON 형식의 메시지를 parse 해서 사용한다. function onMessageReceived(payload) { - //console.log("payload 들어오냐? :"+payload); - var chat = JSON.parse(payload.body); + let chat = JSON.parse(payload.body); + let messageElement = document.createElement('li'); - var messageElement = document.createElement('li'); + addMessageToTheChatRoom(chat, messageElement); +} - if (chat.type === 'ENTER') { // chatType 이 enter 라면 아래 내용 - messageElement.classList.add('event-message'); - chat.content = chat.sender + chat.message; - getUserList(); +// 채팅방 입장 전에, 이전에 대화를 나눴던 메시지들을 보여주도록 처리하는 함수 +function previousMessageReceived(message) { + let chat = JSON.parse(message); + let messageElement = document.createElement('li'); + + addMessageToTheChatRoom(chat, messageElement); +} - } else if (chat.type === 'LEAVE') { // chatType 가 leave 라면 아래 내용 +function addMessageToTheChatRoom(chat, messageElement) { + if (chat.type === 'ENTER' || chat.type === 'LEAVE') { // chatType 이 enter 라면 아래 내용 messageElement.classList.add('event-message'); - chat.content = chat.sender + chat.message; getUserList(); } else { // chatType 이 talk 라면 아래 내용 messageElement.classList.add('chat-message'); - var avatarElement = document.createElement('i'); - var avatarText = document.createTextNode(chat.sender[0]); + let avatarElement = document.createElement('i'); + let avatarText = document.createTextNode(chat.sender[0]); avatarElement.appendChild(avatarText); avatarElement.style['background-color'] = getAvatarColor(chat.sender); messageElement.appendChild(avatarElement); - var usernameElement = document.createElement('span'); - var usernameText = document.createTextNode(chat.sender); + let usernameElement = document.createElement('span'); + let usernameText = document.createTextNode(chat.sender); usernameElement.appendChild(usernameText); messageElement.appendChild(usernameElement); } - var contentElement = document.createElement('p'); + let contentElement = document.createElement('p'); // 만약 s3DataUrl 의 값이 null 이 아니라면 => chat 내용이 파일 업로드와 관련된 내용이라면 // img 를 채팅에 보여주는 작업 if(chat.s3DataUrl != null){ - var imgElement = document.createElement('img'); + let imgElement = document.createElement('img'); imgElement.setAttribute("src", chat.s3DataUrl); imgElement.setAttribute("width", "300"); imgElement.setAttribute("height", "300"); - var downBtnElement = document.createElement('button'); + let downBtnElement = document.createElement('button'); downBtnElement.setAttribute("class", "btn fa fa-download"); downBtnElement.setAttribute("id", "downBtn"); downBtnElement.setAttribute("name", chat.fileName); @@ -166,7 +202,7 @@ function onMessageReceived(payload) { }else{ // 만약 s3DataUrl 의 값이 null 이라면 // 이전에 넘어온 채팅 내용 보여주기 - var messageText = document.createTextNode(chat.message); + let messageText = document.createTextNode(chat.message); contentElement.appendChild(messageText); } diff --git a/src/main/resources/templates/page/chatting/chatroom.html b/src/main/resources/templates/page/chatting/chatroom.html index 31c01d2d..9a3da25c 100644 --- a/src/main/resources/templates/page/chatting/chatroom.html +++ b/src/main/resources/templates/page/chatting/chatroom.html @@ -50,6 +50,7 @@

Sorry! Your browser doesn't support Javascript

+