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 6b4e2ecc..9c60148c 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 @@ -1,5 +1,6 @@ package com.dku.council.domain.chat.controller; +import com.dku.council.domain.chat.model.FileType; import com.dku.council.domain.chat.model.MessageType; import com.dku.council.domain.chat.model.dto.Message; import com.dku.council.domain.chat.model.dto.request.RequestChatDto; @@ -87,7 +88,7 @@ public void enterUser(@Payload RequestChatDto chat, LocalDateTime messageTime = LocalDateTime.now(); // 입장 메시지 저장 - chatRoomMessageService.create(chat.getRoomId(), chat.getType().toString(), chat.getUserId(), chat.getSender(), enterMessage, messageTime); + chatRoomMessageService.create(chat.getRoomId(), chat.getType().toString(), chat.getUserId(), chat.getSender(), enterMessage, messageTime, "", "", chat.getFileType().toString()); Message message = Message.builder() .type(chat.getType()) @@ -95,6 +96,7 @@ public void enterUser(@Payload RequestChatDto chat, .sender(chat.getSender()) .message(enterMessage) .messageTime(messageTime) + .fileType(chat.getFileType()) .build(); sender.send(topic, message); @@ -110,7 +112,7 @@ public void enterUser(@Payload RequestChatDto chat, public void sendMessage(@Payload RequestChatDto chat) { LocalDateTime messageTime = LocalDateTime.now(); - chatRoomMessageService.create(chat.getRoomId(), chat.getType().toString(), chat.getUserId(), chat.getSender(), chat.getMessage(), messageTime); + chatRoomMessageService.create(chat.getRoomId(), chat.getType().toString(), chat.getUserId(), chat.getSender(), chat.getMessage(), messageTime, chat.getFileName(), chat.getFileUrl(), chat.getFileType().toString()); Message message = Message.builder() .type(chat.getType()) @@ -118,6 +120,9 @@ public void sendMessage(@Payload RequestChatDto chat) { .sender(chat.getSender()) .message(chat.getMessage()) .messageTime(messageTime) + .fileName(chat.getFileName()) + .fileUrl(chat.getFileUrl()) + .fileType(chat.getFileType()) .build(); sender.send(topic, message); @@ -182,7 +187,11 @@ public List userList(String roomId) { return chatService.getUserList(roomId); } - + /** + * 채팅방 별, 이전에 나눈 채팅 메시지 리스트 반환 + * + * @param roomId 채팅방 id + */ @GetMapping("/chat/message/list") @UserAuth @ResponseBody diff --git a/src/main/java/com/dku/council/domain/chat/controller/ChatFileController.java b/src/main/java/com/dku/council/domain/chat/controller/ChatFileController.java new file mode 100644 index 00000000..f30f7539 --- /dev/null +++ b/src/main/java/com/dku/council/domain/chat/controller/ChatFileController.java @@ -0,0 +1,79 @@ +package com.dku.council.domain.chat.controller; + +import com.dku.council.domain.chat.exception.InvalidChatRoomUserException; +import com.dku.council.domain.chat.model.dto.request.RequestChatFileDto; +import com.dku.council.domain.chat.service.ChatService; +import com.dku.council.global.auth.jwt.AppAuthentication; +import com.dku.council.global.auth.role.UserAuth; +import com.dku.council.infra.nhn.global.service.service.NHNAuthService; +import com.dku.council.infra.nhn.s3.model.ChatUploadedImage; +import com.dku.council.infra.nhn.s3.model.ImageRequest; +import com.dku.council.infra.nhn.s3.service.ChatImageUploadService; +import com.dku.council.infra.nhn.s3.service.ObjectDownloadService; +import com.dku.council.infra.nhn.s3.service.ObjectStorageService; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.parameters.P; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; +import java.util.List; + +@Tag(name = "채팅방 파일", description = "채팅방 파일/이미지 업로드 및 다운로드 관련 api") +@RestController +@RequestMapping("/chat") +@RequiredArgsConstructor +public class ChatFileController { + + private final ChatService chatService; + private final ChatImageUploadService chatImageUploadService; + private final ObjectDownloadService objectDownloadService; + + private final NHNAuthService nhnAuthService; + private final ObjectStorageService objectStorageService; + +// @PostMapping("/file/upload") + + /** + * 이미지 업로드 기능 + * + * @param request roomId와 전송할 이미지 파일에 대한 dto + */ + @PostMapping(value = "/image/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + @UserAuth + public List uploadImage(AppAuthentication auth, + @Valid @ModelAttribute RequestChatFileDto request) { + // 채팅방에 현재 참여중인 유저가 아니면, 해당 채팅방에 이미지 업로드를 할 수 없게 + if (chatService.alreadyInRoom(request.getRoomId(), auth.getUserId())) { + return chatImageUploadService.newContext().uploadChatImages( + ImageRequest.ofList(request.getFiles()), + request.getRoomId(), + auth.getUserId()); + } else { + throw new InvalidChatRoomUserException(); + } + } + + @GetMapping("/download/{fileName}") + @UserAuth + public ResponseEntity download(AppAuthentication auth, + @PathVariable String fileName, + @RequestParam("roomId") String roomId, + @RequestParam("fileUrl") String fileUrl) { + if (chatService.alreadyInRoom(roomId, auth.getUserId())) { + return objectDownloadService.downloadObject(fileName, fileUrl); + } else { + throw new InvalidChatRoomUserException(); + } + } + + // TODO : 파일 삭제시, chatRoomMessage에도 삭제된게 반영 되어야함 +// @DeleteMapping("/file/delete") +// public void deleteFile(@RequestParam("roomId") String roomId, +// @RequestParam("fileUrl") String fileUrl) { +// objectStorageService.deleteChatFileByDirectUrl(nhnAuthService.requestToken(), fileUrl); +// } + +} 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 c46901a8..12a1837b 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 @@ -1,6 +1,7 @@ package com.dku.council.domain.chat.controller; import com.dku.council.domain.chat.model.dto.response.ResponseChatRoomDto; +import com.dku.council.domain.chat.service.ChatFileService; 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; @@ -10,7 +11,6 @@ import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.*; @@ -22,10 +22,11 @@ @RequiredArgsConstructor @Slf4j public class ChatRoomController { - private final ChatService chatService; private final UserService userService; + private final ChatService chatService; private final ChatRoomMessageService chatRoomMessageService; + private final ChatFileService chatFileService; /** * 채팅방 리스트 화면 @@ -109,10 +110,14 @@ public boolean confirmPwd(@PathVariable String roomId, * * @param roomId 채팅방 id */ - @DeleteMapping("/delete/{roomId}") + @DeleteMapping @UserAuth - public String delChatRoom(@PathVariable String roomId, AppAuthentication auth){ + public String delChatRoom(@RequestParam String roomId, AppAuthentication auth){ + // 해당 채팅방에 존재하는 파일들 삭제 + chatFileService.deleteAllFilesInChatRoom(roomId); + + // 해당 채팅방에 존재하는 채팅 메시지들 삭제 chatRoomMessageService.deleteChatRoomMessages(roomId); // roomId(UUID 값) 기준으로 채팅방 삭제 diff --git a/src/main/java/com/dku/council/domain/chat/exception/InvalidChatRoomUserException.java b/src/main/java/com/dku/council/domain/chat/exception/InvalidChatRoomUserException.java new file mode 100644 index 00000000..ae2122b9 --- /dev/null +++ b/src/main/java/com/dku/council/domain/chat/exception/InvalidChatRoomUserException.java @@ -0,0 +1,8 @@ +package com.dku.council.domain.chat.exception; + +import com.dku.council.global.error.exception.LocalizedMessageException; +import org.springframework.http.HttpStatus; + +public class InvalidChatRoomUserException extends LocalizedMessageException { + public InvalidChatRoomUserException() { super(HttpStatus.BAD_REQUEST, "notfound.chat-room-user"); } +} diff --git a/src/main/java/com/dku/council/domain/chat/model/FileType.java b/src/main/java/com/dku/council/domain/chat/model/FileType.java new file mode 100644 index 00000000..caeac3dd --- /dev/null +++ b/src/main/java/com/dku/council/domain/chat/model/FileType.java @@ -0,0 +1,18 @@ +package com.dku.council.domain.chat.model; + +public enum FileType { + /** + * 이미지 + */ + IMAGE, + + /** + * 파일 + */ + FILE, + + /** + * 일반 메시지 형태일 경우 + */ + NONE +} diff --git a/src/main/java/com/dku/council/domain/chat/model/dto/Message.java b/src/main/java/com/dku/council/domain/chat/model/dto/Message.java index dba682af..184b1839 100644 --- a/src/main/java/com/dku/council/domain/chat/model/dto/Message.java +++ b/src/main/java/com/dku/council/domain/chat/model/dto/Message.java @@ -1,5 +1,6 @@ package com.dku.council.domain.chat.model.dto; +import com.dku.council.domain.chat.model.FileType; import com.dku.council.domain.chat.model.MessageType; import lombok.Builder; import lombok.Getter; @@ -28,16 +29,28 @@ public class Message { @NotNull private LocalDateTime messageTime; + private String fileName; + + private String fileUrl; + + private FileType fileType; + @Builder private Message(MessageType type, String roomId, String sender, String message, - LocalDateTime messageTime) { + LocalDateTime messageTime, + String fileName, + String fileUrl, + FileType fileType) { this.type = type; this.roomId = roomId; this.sender = sender; this.message = message; this.messageTime = messageTime; + this.fileName = fileName; + this.fileUrl = fileUrl; + this.fileType = fileType; } } 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 641e3998..b2e6bfcf 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 @@ -1,5 +1,6 @@ package com.dku.council.domain.chat.model.dto.request; +import com.dku.council.domain.chat.model.FileType; import com.dku.council.domain.chat.model.MessageType; import lombok.Getter; import lombok.RequiredArgsConstructor; @@ -18,10 +19,7 @@ public class RequestChatDto { private final String message; -// private String time; -// -// /* 파일 업로드 관련 변수 (일단 보류) */ -// private String s3DataUrl; // 파일 업로드 url -// private String fileName; // 파일이름 -// private String fileDir; // s3 파일 경로 + private final String fileName; // 파일이름 + private final String fileUrl; // s3에 업로드 된 위치 + private final FileType fileType; // 이미지인지 파일인지 } diff --git a/src/main/java/com/dku/council/domain/chat/model/dto/request/RequestChatFileDto.java b/src/main/java/com/dku/council/domain/chat/model/dto/request/RequestChatFileDto.java new file mode 100644 index 00000000..54ff1fda --- /dev/null +++ b/src/main/java/com/dku/council/domain/chat/model/dto/request/RequestChatFileDto.java @@ -0,0 +1,26 @@ +package com.dku.council.domain.chat.model.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import org.springframework.web.multipart.MultipartFile; + +import javax.validation.constraints.NotBlank; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +@Getter +public class RequestChatFileDto { + + @NotBlank + @Schema(description = "채팅방 번호", example = "d118101z-c737-4253-9911-ea2579405f42") + private final String roomId; + + @Schema(description = "첨부 파일 목록") + private final List files; + + public RequestChatFileDto(String roomId, List files) { + this.roomId = roomId; + this.files = Objects.requireNonNullElseGet(files, ArrayList::new); + } +} diff --git a/src/main/java/com/dku/council/domain/chat/service/ChatFileService.java b/src/main/java/com/dku/council/domain/chat/service/ChatFileService.java new file mode 100644 index 00000000..dcce2d16 --- /dev/null +++ b/src/main/java/com/dku/council/domain/chat/service/ChatFileService.java @@ -0,0 +1,39 @@ +package com.dku.council.domain.chat.service; + +import com.dku.council.domain.chatmessage.model.entity.ChatRoomMessage; +import com.dku.council.domain.chatmessage.repository.ChatRoomMessageRepository; +import com.dku.council.infra.nhn.global.service.service.NHNAuthService; +import com.dku.council.infra.nhn.s3.service.ObjectStorageService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@RequiredArgsConstructor +@Slf4j +public class ChatFileService { + + private final NHNAuthService nhnAuthService; + private final ObjectStorageService objectStorageService; + + private final ChatRoomMessageRepository chatRoomMessageRepository; + + public void deleteAllFilesInChatRoom(String roomId) { + List imageMessageList = chatRoomMessageRepository.findAllByRoomIdAndFileType(roomId, "IMAGE"); + List fileMessageList = chatRoomMessageRepository.findAllByRoomIdAndFileType(roomId, "FILE"); + + if (!imageMessageList.isEmpty()) { + for (ChatRoomMessage chatRoomMessage : imageMessageList) { + objectStorageService.deleteChatFileByDirectUrl(nhnAuthService.requestToken(), chatRoomMessage.getFileUrl()); + } + } + + if (!fileMessageList.isEmpty()) { + for (ChatRoomMessage chatRoomMessage : fileMessageList) { + objectStorageService.deleteChatFileByDirectUrl(nhnAuthService.requestToken(), chatRoomMessage.getFileUrl()); + } + } + } +} diff --git a/src/main/java/com/dku/council/domain/chat/service/ChatService.java b/src/main/java/com/dku/council/domain/chat/service/ChatService.java index babca038..e63365c5 100644 --- a/src/main/java/com/dku/council/domain/chat/service/ChatService.java +++ b/src/main/java/com/dku/council/domain/chat/service/ChatService.java @@ -94,12 +94,6 @@ public ResponseChatRoomDto createChatRoom(String roomName, String roomPwd, boole .build(); chatRoomRepository.save(chatRoom); - ChatRoomUser chatRoomUser = ChatRoomUser.builder() - .chatRoom(chatRoom) - .user(user) - .build(); - chatRoomUserRepository.save(chatRoomUser); - return new ResponseChatRoomDto(chatRoom.getRoomId(), chatRoom.getRoomName(), chatRoom.getUserCount(), @@ -153,6 +147,14 @@ public String addUser(String roomId, String userName){ return user.getNickname(); } + /** + * 특정 채팅방에 참여중인 유저인지 확인 + */ + public boolean alreadyInRoom(String roomId, Long userId) { + long chatRoomId = chatRoomRepository.findChatRoomByRoomId(roomId).orElseThrow(ChatRoomNotFoundException::new).getId(); + return chatRoomUserRepository.existsUserByRoomIdAndUserId(chatRoomId, userId).isPresent(); + } + /** * 채팅방 유저 리스트 삭제 */ @@ -204,9 +206,4 @@ public void delChatRoom(Long userId, String roomId, boolean isAdmin) { } log.info("삭제 완료 roomId : {}", roomId); } - - public boolean alreadyInRoom(String roomId, Long userId) { - long chatRoomId = chatRoomRepository.findChatRoomByRoomId(roomId).orElseThrow(ChatRoomNotFoundException::new).getId(); - return chatRoomUserRepository.existsUserByRoomIdAndUserId(chatRoomId, userId).isPresent(); - } } 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 index e6f18d2c..69d0b4ba 100644 --- 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 @@ -58,4 +58,13 @@ public void setCreatedAt(LocalDateTime createdAt) { @DynamoDBAttribute private String content; + + @DynamoDBAttribute + private String fileName; + + @DynamoDBAttribute + private String fileUrl; + + @DynamoDBAttribute + private String fileType; } \ 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 index 17d253ef..b86427fd 100644 --- a/src/main/java/com/dku/council/domain/chatmessage/repository/ChatRoomMessageRepository.java +++ b/src/main/java/com/dku/council/domain/chatmessage/repository/ChatRoomMessageRepository.java @@ -14,4 +14,6 @@ public interface ChatRoomMessageRepository extends CrudRepository findAllByRoomIdOrderByCreatedAtAsc(String roomId); void deleteAllByRoomId(String roomId); + + List findAllByRoomIdAndFileType(String roomId, String fileType); } 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 index eb553566..b2b85b26 100644 --- a/src/main/java/com/dku/council/domain/chatmessage/service/ChatRoomMessageService.java +++ b/src/main/java/com/dku/council/domain/chatmessage/service/ChatRoomMessageService.java @@ -24,7 +24,10 @@ public void create(String roomId, Long userId, String userNickname, String content, - LocalDateTime messageTime) { + LocalDateTime messageTime, + String fileName, + String fileUrl, + String fileType) { ChatRoomMessage chatRoomMessage = new ChatRoomMessage(); chatRoomMessage.setRoomId(roomId); @@ -33,6 +36,9 @@ public void create(String roomId, chatRoomMessage.setUserNickname(userNickname); chatRoomMessage.setContent(content); chatRoomMessage.setCreatedAt(messageTime.atZone(seoulZoneId).toLocalDateTime()); + chatRoomMessage.setFileName(fileName); + chatRoomMessage.setFileUrl(fileUrl); + chatRoomMessage.setFileType(fileType); chatRoomMessageRepository.save(chatRoomMessage); } diff --git a/src/main/java/com/dku/council/infra/nhn/s3/model/ChatUploadedImage.java b/src/main/java/com/dku/council/infra/nhn/s3/model/ChatUploadedImage.java new file mode 100644 index 00000000..12c121a2 --- /dev/null +++ b/src/main/java/com/dku/council/infra/nhn/s3/model/ChatUploadedImage.java @@ -0,0 +1,26 @@ +package com.dku.council.infra.nhn.s3.model; + +import com.dku.council.domain.chat.model.FileType; +import lombok.Getter; + +@Getter +public class ChatUploadedImage { + + private final String roomId; + + private final Long userId; + + private final String fileName; + + private final String fileUrl; + + private final String fileType; + + public ChatUploadedImage(String roomId, Long userId, String fileUrl, ImageRequest image) { + this.roomId = roomId; + this.userId = userId; + this.fileName = image.getOriginalFilename(); + this.fileUrl = fileUrl; + this.fileType = FileType.IMAGE.toString(); + } +} diff --git a/src/main/java/com/dku/council/infra/nhn/s3/service/ChatFileUploadService.java b/src/main/java/com/dku/council/infra/nhn/s3/service/ChatFileUploadService.java new file mode 100644 index 00000000..b60bacfd --- /dev/null +++ b/src/main/java/com/dku/council/infra/nhn/s3/service/ChatFileUploadService.java @@ -0,0 +1,64 @@ +package com.dku.council.infra.nhn.s3.service; + +import com.dku.council.infra.nhn.global.exception.InvalidAccessObjectStorageException; +import com.dku.council.infra.nhn.global.service.service.NHNAuthService; +import com.dku.council.infra.nhn.s3.model.FileRequest; +import com.dku.council.infra.nhn.s3.model.UploadedFile; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class ChatFileUploadService { + + private final NHNAuthService nhnAuthService; + private final ObjectStorageService s3service; + private final ObjectUploadContext uploadContext; + + public Context newContext() { + String token = nhnAuthService.requestToken(); + return new Context(token); + } + + public class Context { + private final String token; + + private Context(String token) { this.token = token; } + + public ArrayList uploadChatFiles(List files, String roomId, String prefix) { + ArrayList chatFiles = new ArrayList<>(); + for (FileRequest req : files) { + chatFiles.add(uploadChatFile(req, roomId, prefix)); + } + return chatFiles; + } + + public UploadedFile uploadChatFile(FileRequest file, String roomId, String prefix) { + String originName = file.getOriginalFilename(); + if (originName == null) originName = ""; + + String ext = originName.substring(originName.lastIndexOf(".") + 1); + String fileId; + do { + fileId = uploadContext.makeFileId(prefix, ext); + } while (s3service.isInChatFilePath(roomId, fileId)); + return upload(file, roomId, fileId); + } + + private UploadedFile upload(FileRequest file, String roomId, String objectName) { + try { + s3service.uploadChatFile(token, roomId, objectName, file.getInputStream(), file.getContentType()); + return new UploadedFile(objectName, file); + } catch (Throwable e) { + throw new InvalidAccessObjectStorageException(e); + } + } + + public void deleteChatFile(String roomId, String fileUrl) { + s3service.deleteChatFileByDirectUrl(token, fileUrl); + } + } +} diff --git a/src/main/java/com/dku/council/infra/nhn/s3/service/ChatImageUploadService.java b/src/main/java/com/dku/council/infra/nhn/s3/service/ChatImageUploadService.java new file mode 100644 index 00000000..986a3387 --- /dev/null +++ b/src/main/java/com/dku/council/infra/nhn/s3/service/ChatImageUploadService.java @@ -0,0 +1,76 @@ +package com.dku.council.infra.nhn.s3.service; + +import com.dku.council.infra.nhn.global.exception.AlreadyInStorageException; +import com.dku.council.infra.nhn.global.service.service.NHNAuthService; +import com.dku.council.infra.nhn.s3.model.ImageRequest; +import com.dku.council.infra.nhn.s3.model.ChatUploadedImage; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class ChatImageUploadService { + + private final NHNAuthService nhnAuthService; + private final ObjectUploadContext uploadContext; + private final ObjectStorageService s3service; + + public Context newContext() { + String token = nhnAuthService.requestToken(); + return new Context(token); + } + + public class Context { + private final String token; + + private Context(String token) { this.token = token; } + + public ArrayList uploadChatImages(List images, String roomId, Long userId) { + ArrayList chatImages = new ArrayList<>(); + for (ImageRequest req : images) { + chatImages.add(uploadChatImage(req, roomId, userId)); + } + return chatImages; + } + + public ChatUploadedImage uploadChatImage(ImageRequest image, String roomId, Long userId) { + String originName = image.getOriginalFilename(); + if (originName == null) originName = ""; + + String prefix = originName.substring(0, originName.lastIndexOf(".")); + String ext = originName.substring(originName.lastIndexOf(".") + 1); + String imageId; + do { + imageId = uploadContext.makeImageId(prefix, ext); + } while (s3service.isInChatImagePath(roomId, imageId)); + return upload(image, roomId, userId, imageId); + } + + public ChatUploadedImage uploadChatImageWithName(ImageRequest image, String roomId, Long userId, String objectName) { + if (s3service.isInChatImagePath(roomId, objectName)) { + throw new AlreadyInStorageException(); + } + return upload(image, roomId, userId, objectName); + } + + private ChatUploadedImage upload(ImageRequest image, String roomId, Long userId, String objectName) { + try { + s3service.uploadChatImage(token, roomId, objectName, image.getInputStream(), image.getContentType()); + String chatImageUrl = uploadContext.getChatImageUrl(roomId, objectName); + + // TODO : 여기다가 ChatFile에 담는 서비스 로직 추가해주면될듯? + + return new ChatUploadedImage(roomId, userId, chatImageUrl, image); + } catch (Throwable e) { + throw new RuntimeException(e); + } + } + + public void deleteChatImage(String fileUrl) { + s3service.deleteChatFileByDirectUrl(token, fileUrl); + } + } +} diff --git a/src/main/java/com/dku/council/infra/nhn/s3/service/ObjectDownloadService.java b/src/main/java/com/dku/council/infra/nhn/s3/service/ObjectDownloadService.java new file mode 100644 index 00000000..98998f50 --- /dev/null +++ b/src/main/java/com/dku/council/infra/nhn/s3/service/ObjectDownloadService.java @@ -0,0 +1,39 @@ +package com.dku.council.infra.nhn.s3.service; + +import com.dku.council.infra.nhn.global.service.service.NHNAuthService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.*; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.util.List; +import java.util.Objects; + +@Service +@RequiredArgsConstructor +public class ObjectDownloadService { + + private final NHNAuthService nhnAuthService; + + public ResponseEntity downloadObject(String fileName, String fileUrl) { + + // RestTemplate 생성 + RestTemplate restTemplate = new RestTemplate(); + + // 헤더 생성 + HttpHeaders headers = new HttpHeaders(); + headers.add("X-Auth-Token", nhnAuthService.requestToken()); + headers.setAccept(List.of(MediaType.APPLICATION_OCTET_STREAM)); + headers.setContentDispositionFormData("attachment", fileName); + + HttpEntity requestHttpEntity = new HttpEntity(null, headers); + + // API 호출, 데이터를 바이트 배열로 받음 + ResponseEntity response = restTemplate.exchange(fileUrl, HttpMethod.GET, requestHttpEntity, byte[].class); + + return response; + } + +} diff --git a/src/main/java/com/dku/council/infra/nhn/s3/service/ObjectStorageService.java b/src/main/java/com/dku/council/infra/nhn/s3/service/ObjectStorageService.java index 198a5d54..355da145 100644 --- a/src/main/java/com/dku/council/infra/nhn/s3/service/ObjectStorageService.java +++ b/src/main/java/com/dku/council/infra/nhn/s3/service/ObjectStorageService.java @@ -72,6 +72,43 @@ public boolean isInFilePath(String objectName) { return true; } + /** + * ~/chatroom/roomId/image/objectName 경로에 파일이 있는지 확인합니다. + */ + public boolean isInChatImagePath(String roomId, String objectName) { + try { + webClient.get() + .uri(uploadContext.getChatImageUrl(roomId, objectName)) + .retrieve() + .bodyToMono(byte[].class) + .block(); + } catch (WebClientResponseException e) { + if (e.getStatusCode() == HttpStatus.NOT_FOUND) { + return false; + } + } + return true; + } + + /** + * ~/chatroom/roomId/file/objectName 경로에 파일이 있는지 확인합니다. + */ + public boolean isInChatFilePath(String roomId, String objectName) { + try { + webClient.get() + .uri(uploadContext.getChatFileUrl(roomId, objectName)) + .retrieve() + .bodyToMono(byte[].class) + .block(); + } catch (WebClientResponseException e) { + if (e.getStatusCode() == HttpStatus.NOT_FOUND) { + return false; + } + } + return true; + } + + //TODO 기존 코드 삭제 예정 public void uploadObject(String tokenId, String objectName, final InputStream inputStream, @Nullable MediaType contentType) { try { @@ -136,6 +173,50 @@ public void uploadFile(String tokenId, String objectName, final InputStream inpu } } + /** + * ~/chatroom/roomId/image/objectName 경로에 파일을 업로드합니다. + */ + public void uploadChatImage(String tokenId, String roomId, String objectName, final InputStream inputStream, @Nullable MediaType contentType) { + try { + WebClient.RequestBodySpec spec = webClient.put() + .uri(uploadContext.getChatImageUrl(roomId, objectName)) + .header("X-Auth-Token", tokenId); + + if (contentType != null) { + spec = spec.header("Content-Type", contentType.toString()); + } + + spec.body(BodyInserters.fromResource(new InputStreamResource(inputStream))) + .retrieve() + .bodyToMono(Void.class) + .block(); + } catch (Throwable e) { + throw new InvalidAccessObjectStorageException(e); + } + } + + /** + * ~/chatroom/roomId/file/objectName 경로에 파일을 업로드합니다. + */ + public void uploadChatFile(String tokenId, String roomId, String objectName, final InputStream inputStream, @Nullable MediaType contentType) { + try { + WebClient.RequestBodySpec spec = webClient.put() + .uri(uploadContext.getChatFileUrl(roomId, objectName)) + .header("X-Auth-Token", tokenId); + + if (contentType != null) { + spec = spec.header("Content-Type", contentType.toString()); + } + + spec.body(BodyInserters.fromResource(new InputStreamResource(inputStream))) + .retrieve() + .bodyToMono(Void.class) + .block(); + } catch (Throwable e) { + throw new InvalidAccessObjectStorageException(e); + } + } + //TODO 기존 코드 삭제 예정 public void deleteObject(String tokenId, String objectName) { try { @@ -182,4 +263,19 @@ public void deleteFile(String tokenId, String objectName) { } } + /** + * 특정 채팅방에 존재하는 특정 파일을 삭제합니다. + */ + public void deleteChatFileByDirectUrl(String tokenId, String fileUrl) { + try { + webClient.delete() + .uri(fileUrl) + .header("X-Auth-Token", tokenId) + .retrieve() + .bodyToMono(Void.class) + .block(); + } catch (Throwable e) { + throw new InvalidAccessObjectStorageException(e); + } + } } diff --git a/src/main/java/com/dku/council/infra/nhn/s3/service/ObjectUploadContext.java b/src/main/java/com/dku/council/infra/nhn/s3/service/ObjectUploadContext.java index bc285907..ed2838c4 100644 --- a/src/main/java/com/dku/council/infra/nhn/s3/service/ObjectUploadContext.java +++ b/src/main/java/com/dku/council/infra/nhn/s3/service/ObjectUploadContext.java @@ -23,6 +23,11 @@ public class ObjectUploadContext { @Value("${app.post.thumbnail.default}") private final String defaultThumbnailId; + @Value("${nhn.os.api-chat-image-path}") + private final String apiChatImagePath; + + @Value("${nhn.os.api-chat-file-path}") + private final String apiChatFilePath; public String getThumbnailUrl(String thumbnailId) { if (thumbnailId == null || thumbnailId.isBlank()) { @@ -44,6 +49,9 @@ public String getFileUrl(String objectName) { return String.format(apiFilePath, objectName); } + public String getChatImageUrl(String roomId, String objectName) { return String.format(apiChatImagePath, roomId, objectName); } + public String getChatFileUrl(String roomId, String objectName) { return String.format(apiChatFilePath, roomId, objectName); } + public String makeObjectId(String prefix, String extension) { return makeObjName(prefix, UUID.randomUUID() + "." + extension); } diff --git a/src/main/resources/errors.properties b/src/main/resources/errors.properties index ea04901a..9ff0406a 100644 --- a/src/main/resources/errors.properties +++ b/src/main/resources/errors.properties @@ -52,6 +52,7 @@ invalid.student-id=\uCD5C\uC18C \uD559\uBC88\uC5D0 \uD574\uB2F9\uD558\uC9C0 \uC5 invalid.status=\uCC38\uC5EC\uAC00 \uBD88\uAC00\uB2A5\uD569\uB2C8\uB2E4. invalid.create-review-to-myself=\uC790\uC2E0\uC5D0\uAC8C\uB294 \uB9AC\uBDF0 \uC791\uC131\uC744 \uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. invalid.mbti=MBTI\uAC00 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. +invalid.chat-room-user=\uD574\uB2F9 \uCC44\uD305\uBC29\uC5D0\uC11C \uCC38\uC5EC\uC911\uC778 \uC720\uC800\uAC00 \uC544\uB2D9\uB2C8\uB2E4. notfound.user=\uC720\uC800\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. notfound.post=\uD574\uB2F9 \uAC8C\uC2DC\uAE00\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. diff --git a/src/main/resources/errors_en_US.properties b/src/main/resources/errors_en_US.properties index a9852bc0..9bcdfdad 100644 --- a/src/main/resources/errors_en_US.properties +++ b/src/main/resources/errors_en_US.properties @@ -52,6 +52,7 @@ invalid.student-id=It does not correspond to the minimum number of students. invalid.status=You can't enter because of your status. invalid.create-review-to-myself=You can't write a review to yourself. invalid.mbti=Invalid MBTI. +invalid.chat-room-user=You are not a participating user in this chat room. notfound.user=Cannot find that user. notfound.post=No such post was found. diff --git a/src/main/resources/static/js/chatroom/socket.js b/src/main/resources/static/js/chatroom/socket.js index 75d764c9..d4d0ad9d 100644 --- a/src/main/resources/static/js/chatroom/socket.js +++ b/src/main/resources/static/js/chatroom/socket.js @@ -60,7 +60,8 @@ function onConnected() { "roomId": roomId, "userId": userId, sender: username, - type: 'ENTER' + type: 'ENTER', + fileType: 'NONE' }) ) @@ -107,7 +108,10 @@ function getPreviousMessageList() { sender: data[i]["userNickname"], message: data[i]["content"], type: data[i]["messageType"], - messageTime: data[i]["createdAt"] + messageTime: data[i]["createdAt"], + "fileName": data[i]["fileName"], + fileUrl: data[i]["fileUrl"], + fileType: data[i]["fileType"] }; console.log(previousChatMessage); previousMessageReceived(JSON.stringify(previousChatMessage)); @@ -132,7 +136,8 @@ function sendMessage(event) { "userId": userId, sender: username, message: messageInput.value, - type: 'TALK' + type: 'TALK', + fileType: 'NONE' }; stompClient.send("/pub/chat/sendMessage", {}, JSON.stringify(chatMessage)); @@ -189,9 +194,9 @@ function addMessageToTheChatRoom(chat, messageElement) { // 만약 s3DataUrl 의 값이 null 이 아니라면 => chat 내용이 파일 업로드와 관련된 내용이라면 // img 를 채팅에 보여주는 작업 - if(chat.s3DataUrl != null){ + if(chat.fileUrl !== "" && chat.fileType === 'IMAGE'){ let imgElement = document.createElement('img'); - imgElement.setAttribute("src", chat.s3DataUrl); + imgElement.setAttribute("src", chat.fileUrl); imgElement.setAttribute("width", "300"); imgElement.setAttribute("height", "300"); @@ -199,8 +204,7 @@ function addMessageToTheChatRoom(chat, messageElement) { downBtnElement.setAttribute("class", "btn fa fa-download"); downBtnElement.setAttribute("id", "downBtn"); downBtnElement.setAttribute("name", chat.fileName); - downBtnElement.setAttribute("onclick", `downloadFile('${chat.fileName}', '${chat.fileDir}')`); - + downBtnElement.setAttribute("onclick", `downloadFile('${chat.roomId}', '${chat.fileName}', '${chat.fileUrl}')`); contentElement.appendChild(imgElement); contentElement.appendChild(downBtnElement); @@ -255,75 +259,72 @@ document.addEventListener('DOMContentLoaded', function () { messageForm.addEventListener('submit', sendMessage, true) -// TODO: 아래 파일 관련 내용들은 일단 보류 - /// 파일 업로드 부분 //// function uploadFile(){ - // var file = $("#file")[0].files[0]; - // var formData = new FormData(); - // formData.append("file",file); - // formData.append("roomId", roomId); - // - // // ajax 로 multipart/form-data 를 넘겨줄 때는 - // // processData: false, - // // contentType: false - // // 처럼 설정해주어야 한다. - // - // // 동작 순서 - // // post 로 rest 요청한다. - // // 1. 먼저 upload 로 파일 업로드를 요청한다. - // // 2. upload 가 성공적으로 완료되면 data 에 upload 객체를 받고, - // // 이를 이용해 chatMessage 를 작성한다. - // $.ajax({ - // type : 'POST', - // url : '/s3/upload', - // data : formData, - // processData: false, - // contentType: false - // }).done(function (data){ - // // console.log("업로드 성공") - // - // var chatMessage = { - // "roomId": roomId, - // sender: username, - // message: username+"님의 파일 업로드", - // type: 'TALK', - // s3DataUrl : data.s3DataUrl, // Dataurl - // "fileName": file.name, // 원본 파일 이름 - // "fileDir": data.fileDir // 업로드 된 위치 - // }; - // - // // 해당 내용을 발신한다. - // stompClient.send("/pub/chat/sendMessage", {}, JSON.stringify(chatMessage)); - // }).fail(function (error){ - // alert(error); - // }) + var files = $("#file")[0].files; + var formData = new FormData(); + + for (let i = 0; i < files.length; i++) { + formData.append("files", files[i]); + } + formData.append("roomId", roomId); + + // ajax 로 multipart/form-data 를 넘겨줄 때는 + // processData: false, + // contentType: false + // 처럼 설정해주어야 한다. + + // 동작 순서 + // post 로 rest 요청한다. + // 1. 먼저 upload 로 파일 업로드를 요청한다. + // 2. upload 가 성공적으로 완료되면 data 에 upload 객체를 받고, + // 이를 이용해 chatMessage 를 작성한다. + $.ajax({ + type : 'POST', + url : '/chat/image/upload', + data : formData, + processData: false, + contentType: false + }).done(function (data){ + for (let i = 0; data.length; i++) { + var chatMessage = { + "roomId": roomId, + "userId": data[i]["userId"], + sender: username, + type: 'TALK', + "fileName": data[i]["fileName"], // 원본 파일 이름 + fileUrl: data[i]["fileUrl"], // 저장된 파일 경로 + fileType: data[i]["fileType"] + }; + + // 해당 내용을 발신한다. + stompClient.send("/pub/chat/sendMessage", {}, JSON.stringify(chatMessage)); + } + }).fail(function (error){ + alert(error); + }) } // 파일 다운로드 부분 // // 버튼을 누르면 downloadFile 메서드가 실행됨 -// 다운로드 url 은 /s3/download+원본파일이름 -function downloadFile(name, dir){ - // // console.log("파일 이름 : "+name); - // // console.log("파일 경로 : " + dir); - // let url = "/s3/download/"+name; - // - // // get 으로 rest 요청한다. - // $.ajax({ - // url: "/s3/download/"+name, // 요청 url 은 download/{name} - // data: { - // "fileDir" : dir // 파일의 경로를 파라미터로 넣는다. - // }, - // dataType: 'binary', // 파일 다운로드를 위해서는 binary 타입으로 받아야한다. - // xhrFields: { - // 'responseType': 'blob' // 여기도 마찬가지 - // }, - // success: function(data) { - // - // var link = document.createElement('a'); - // link.href = URL.createObjectURL(data); - // link.download = name; - // link.click(); - // } - // }); +// 다운로드 url 은 /chat/download+원본파일이름 +function downloadFile(roomId, fileName, url){ + // get 으로 rest 요청한다. + $.ajax({ + url: "/chat/download/"+fileName, // 요청 url 은 download/{name} + data: { + "roomId": roomId, + "fileUrl" : url + }, + dataType: 'binary', // 파일 다운로드를 위해서는 binary 타입으로 받아야한다. + xhrFields: { + 'responseType': 'blob' // 여기도 마찬가지 + }, + success: function(data) { + var link = document.createElement('a'); + link.href = URL.createObjectURL(data); + link.download = fileName; + link.click(); + } + }); } \ No newline at end of file diff --git a/src/main/resources/templates/page/chatting/roomlist.html b/src/main/resources/templates/page/chatting/roomlist.html index 49e91571..43102386 100644 --- a/src/main/resources/templates/page/chatting/roomlist.html +++ b/src/main/resources/templates/page/chatting/roomlist.html @@ -163,16 +163,14 @@ }) } - // 채팅방 삭제 - // function delRoom(){ - // location.href = "/chatRoom/delete/"+roomId; - // } - function delRoom(){ $.ajax({ type : "DELETE", - url : "/chatRoom/delete/"+roomId, + url : "/chatRoom", async : false, + data: { + "roomId": roomId + }, success : function(result){ if (result) { diff --git a/src/test/java/com/dku/council/domain/post/service/PetitionServiceTest.java b/src/test/java/com/dku/council/domain/post/service/PetitionServiceTest.java index 651739b9..80b7cb45 100644 --- a/src/test/java/com/dku/council/domain/post/service/PetitionServiceTest.java +++ b/src/test/java/com/dku/council/domain/post/service/PetitionServiceTest.java @@ -47,7 +47,7 @@ public class PetitionServiceTest { private final Clock clock = ClockUtil.create(); private final Duration writeCooltime = Duration.ofDays(1); - private final ObjectUploadContext uploadContext = new ObjectUploadContext("", "", "", ""); + private final ObjectUploadContext uploadContext = new ObjectUploadContext("", "", "", "", "", ""); @Mock private PetitionStatisticService petitionStatisticService; diff --git a/src/test/java/com/dku/council/infra/nhn/service/ObjectStorageServiceTest.java b/src/test/java/com/dku/council/infra/nhn/service/ObjectStorageServiceTest.java index 968dfca9..2917c03e 100644 --- a/src/test/java/com/dku/council/infra/nhn/service/ObjectStorageServiceTest.java +++ b/src/test/java/com/dku/council/infra/nhn/service/ObjectStorageServiceTest.java @@ -26,13 +26,17 @@ class ObjectStorageServiceTest extends AbstractMockServerTest { private String apiFilePath; + private String apiChatImagePath; + + private String apiChatFilePath; + @BeforeEach public void beforeEach() { WebClient webClient = WebClient.create(); this.apiPath = "http://localhost:" + mockServer.getPort(); - this.uploadContext = new ObjectUploadContext(apiPath, apiImagePath, apiFilePath ,"default"); + this.uploadContext = new ObjectUploadContext(apiPath, apiImagePath, apiFilePath ,"default", apiChatImagePath, apiChatFilePath); this.service = new ObjectStorageService(webClient, uploadContext); } diff --git a/src/test/java/com/dku/council/infra/nhn/service/actual/ActualObjectStorageServiceTest.java b/src/test/java/com/dku/council/infra/nhn/service/actual/ActualObjectStorageServiceTest.java index 72e8f040..0b6533c3 100644 --- a/src/test/java/com/dku/council/infra/nhn/service/actual/ActualObjectStorageServiceTest.java +++ b/src/test/java/com/dku/council/infra/nhn/service/actual/ActualObjectStorageServiceTest.java @@ -36,12 +36,14 @@ public void beforeEach() throws NoSuchMethodException, InvocationTargetException String osApiPath = properties.get("nhn.os.api-path"); String osApiImagePath = properties.get("nhn.os.api-image-path"); String osApiFilePath = properties.get("nhn.os.api-file-path"); + String osApiChatImagePath = properties.get("nhn.os.api-chat-image-path"); + String osApiChatFilePath = properties.get("nhn.os.api-chat-file-path"); String authApiPath = properties.get("nhn.auth.api-path"); String tenantId = properties.get("nhn.auth.tenant-id"); String username = properties.get("nhn.auth.username"); String password = properties.get("nhn.auth.password"); - ObjectUploadContext uploadContext = new ObjectUploadContext(osApiPath, osApiImagePath, osApiFilePath ,defaultThumbnail); + ObjectUploadContext uploadContext = new ObjectUploadContext(osApiPath, osApiImagePath, osApiFilePath ,defaultThumbnail, osApiChatImagePath, osApiChatFilePath); this.storageService = new ObjectStorageService(webClient, uploadContext); this.authService = new NHNAuthService(webClient, authApiPath, tenantId, username, password);