From f6111c9544e614ea58d806453234202fc1ab2540 Mon Sep 17 00:00:00 2001 From: Gimin Kim Date: Thu, 12 Feb 2026 15:31:21 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20ChatErrorCode=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/exception/code/ChatErrorCode.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/main/java/com/cotato/itda/domain/chat/exception/code/ChatErrorCode.java b/src/main/java/com/cotato/itda/domain/chat/exception/code/ChatErrorCode.java index 6b6d423..dda7ccc 100644 --- a/src/main/java/com/cotato/itda/domain/chat/exception/code/ChatErrorCode.java +++ b/src/main/java/com/cotato/itda/domain/chat/exception/code/ChatErrorCode.java @@ -11,6 +11,26 @@ @RequiredArgsConstructor public enum ChatErrorCode implements ErrorCode { + INVALID_MESSAGE_CONTENT( + HttpStatus.BAD_REQUEST, + "메시지 내용이 유효하지 않습니다.", + "CHAT_ERROR_400_INVALID_MESSAGE_CONTENT" + ), + INVALID_ATTACHMENT_FOR_TEXT( + HttpStatus.BAD_REQUEST, + "텍스트 메시지에는 첨부파일이 포함될 수 없습니다.", + "CHAT_ERROR_400_INVALID_ATTACHMENT_FOR_TEXT" + ), + INVALID_ATTACHMENT_REQUIRED( + HttpStatus.BAD_REQUEST, + "첨부파일 메시지에는 첨부파일이 반드시 포함되어야 합니다.", + "CHAT_ERROR_400_INVALID_ATTACHMENT_REQUIRED" + ), + INVALID_ATTACHMENT_OBJECT_KEY( + HttpStatus.BAD_REQUEST, + "첨부파일의 objectKey가 유효하지 않습니다.", + "CHAT_ERROR_400_INVALID_ATTACHMENT_OBJECT_KEY" + ), // 현재 상태가 ACTIVE가 아닙니다 CHAT_ROOM_MEMBER_STATUS_INVALID( HttpStatus.BAD_REQUEST, From 62c07a98366b1c0b14a6fa90408497d2cba0b4e7 Mon Sep 17 00:00:00 2001 From: Gimin Kim Date: Thu, 12 Feb 2026 15:31:52 +0900 Subject: [PATCH 2/3] =?UTF-8?q?feat:=20dto=20=EC=B2=A8=EB=B6=80=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EB=B3=80=EC=88=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/controller/dto/ChatAttachmentRequest.java | 14 ++++++++++++++ .../domain/websocket/dto/ChatRoomMessageDto.java | 3 ++- .../websocket/dto/ChatSendMessageRequest.java | 12 ++++++++++-- 3 files changed, 26 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/cotato/itda/domain/chat/controller/dto/ChatAttachmentRequest.java diff --git a/src/main/java/com/cotato/itda/domain/chat/controller/dto/ChatAttachmentRequest.java b/src/main/java/com/cotato/itda/domain/chat/controller/dto/ChatAttachmentRequest.java new file mode 100644 index 0000000..d3027ff --- /dev/null +++ b/src/main/java/com/cotato/itda/domain/chat/controller/dto/ChatAttachmentRequest.java @@ -0,0 +1,14 @@ +package com.cotato.itda.domain.chat.controller.dto; + +import com.cotato.itda.domain.chat.enums.AttachmentType; + +import jakarta.validation.constraints.NotNull; + +public record ChatAttachmentRequest ( + @NotNull AttachmentType attachmentType, + @NotNull String objectKey, + String mimeType, + Long sizeBytes, + Integer durationMs +){ +} diff --git a/src/main/java/com/cotato/itda/domain/websocket/dto/ChatRoomMessageDto.java b/src/main/java/com/cotato/itda/domain/websocket/dto/ChatRoomMessageDto.java index cd9bcc5..d451809 100644 --- a/src/main/java/com/cotato/itda/domain/websocket/dto/ChatRoomMessageDto.java +++ b/src/main/java/com/cotato/itda/domain/websocket/dto/ChatRoomMessageDto.java @@ -2,6 +2,7 @@ import java.time.LocalDateTime; +import com.cotato.itda.domain.chat.controller.dto.ChatMessageItemDto; import com.cotato.itda.domain.chat.enums.AttachmentStatus; import com.cotato.itda.domain.chat.enums.AttachmentType; import com.cotato.itda.domain.chat.enums.MessageType; @@ -20,7 +21,7 @@ public record ChatRoomMessageDto( MessageType messageType, String content, LocalDateTime createdAt, - Attachment attachment + ChatMessageItemDto.AttachmentMeta attachment ) { @Builder public record Attachment( diff --git a/src/main/java/com/cotato/itda/domain/websocket/dto/ChatSendMessageRequest.java b/src/main/java/com/cotato/itda/domain/websocket/dto/ChatSendMessageRequest.java index b607fba..4e95c91 100644 --- a/src/main/java/com/cotato/itda/domain/websocket/dto/ChatSendMessageRequest.java +++ b/src/main/java/com/cotato/itda/domain/websocket/dto/ChatSendMessageRequest.java @@ -1,19 +1,27 @@ package com.cotato.itda.domain.websocket.dto; +import com.cotato.itda.domain.chat.controller.dto.ChatAttachmentRequest; import com.cotato.itda.domain.chat.enums.MessageType; import jakarta.validation.constraints.NotNull; -import lombok.ToString; /** * 임시 화면(roomId 없음)에서도 보내야 하므로 roomId는 nullable * - clientMessageId: 클라이언트에서 생성한 메시지 ID (중복 방지 및 추적용) + * TEXT + * - content 필수 + * - attachment null + * ATTACHMENT (S3 업로드 완료 후) + * - content null + * - attachment 필수 + * - attachment.objectKey 필수 */ public record ChatSendMessageRequest( @NotNull String clientMessageId, Long roomId, Long opponentMemberId, // roomId가 없을 때 필수 @NotNull MessageType messageType, - String content + String content, + ChatAttachmentRequest attachment ){ } From 551ea473bb1e67caf591a2c2cfaa150a72c41672 Mon Sep 17 00:00:00 2001 From: Gimin Kim Date: Thu, 12 Feb 2026 15:32:11 +0900 Subject: [PATCH 3/3] =?UTF-8?q?feat:=20=EC=B1=84=ED=8C=85=20=EC=B2=A8?= =?UTF-8?q?=EB=B6=80=ED=8C=8C=EC=9D=BC=20=EB=B3=B4=EB=82=B4=EA=B8=B0=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...a => ChatMessageAttachmentRepository.java} | 2 +- .../chat/service/ChatMessageService.java | 65 ++++++++++++++++++- 2 files changed, 64 insertions(+), 3 deletions(-) rename src/main/java/com/cotato/itda/domain/chat/repository/{ChatMessasgeAttachmentRepository.java => ChatMessageAttachmentRepository.java} (63%) diff --git a/src/main/java/com/cotato/itda/domain/chat/repository/ChatMessasgeAttachmentRepository.java b/src/main/java/com/cotato/itda/domain/chat/repository/ChatMessageAttachmentRepository.java similarity index 63% rename from src/main/java/com/cotato/itda/domain/chat/repository/ChatMessasgeAttachmentRepository.java rename to src/main/java/com/cotato/itda/domain/chat/repository/ChatMessageAttachmentRepository.java index 38fb43b..b696207 100644 --- a/src/main/java/com/cotato/itda/domain/chat/repository/ChatMessasgeAttachmentRepository.java +++ b/src/main/java/com/cotato/itda/domain/chat/repository/ChatMessageAttachmentRepository.java @@ -4,5 +4,5 @@ import com.cotato.itda.domain.chat.entity.ChatMessageAttachment; -public interface ChatMessasgeAttachmentRepository extends JpaRepository { +public interface ChatMessageAttachmentRepository extends JpaRepository { } diff --git a/src/main/java/com/cotato/itda/domain/chat/service/ChatMessageService.java b/src/main/java/com/cotato/itda/domain/chat/service/ChatMessageService.java index 6971f0e..6594ee5 100644 --- a/src/main/java/com/cotato/itda/domain/chat/service/ChatMessageService.java +++ b/src/main/java/com/cotato/itda/domain/chat/service/ChatMessageService.java @@ -8,15 +8,19 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import com.cotato.itda.domain.chat.controller.dto.ChatAttachmentRequest; import com.cotato.itda.domain.chat.controller.dto.ChatMessageItemDto; import com.cotato.itda.domain.chat.controller.dto.ChatMessageSliceResponse; import com.cotato.itda.domain.chat.entity.ChatMessage; +import com.cotato.itda.domain.chat.entity.ChatMessageAttachment; import com.cotato.itda.domain.chat.entity.ChatRoom; import com.cotato.itda.domain.chat.entity.ChatRoomMember; +import com.cotato.itda.domain.chat.enums.AttachmentStatus; import com.cotato.itda.domain.chat.enums.LastMessageType; import com.cotato.itda.domain.chat.enums.MemberRoomStatus; import com.cotato.itda.domain.chat.enums.MessageType; import com.cotato.itda.domain.chat.exception.code.ChatErrorCode; +import com.cotato.itda.domain.chat.repository.ChatMessageAttachmentRepository; import com.cotato.itda.domain.chat.repository.ChatRoomMemberRepository; import com.cotato.itda.domain.chat.repository.ChatRoomQueryRepository; import com.cotato.itda.domain.chat.repository.ChatRoomRepository; @@ -44,7 +48,7 @@ public class ChatMessageService { private final ChatRoomMemberRepository chatRoomMemberRepository; private final ChatMessageRepository chatMessageRepository; private final ChatRoomQueryRepository chatRoomQueryRepository; - + private final ChatMessageAttachmentRepository chatMessageAttachmentRepository; private final MemberRepository memberRepository; private final SimpMessagingTemplate messagingTemplate; @@ -68,6 +72,7 @@ public void sendMessage(Long senderMemberId, ChatSendMessageRequest req) { throw new BusinessException(ChatErrorCode.INVALID_REQUEST_BOTH_ROOMID_OPPONENTID_NULL); } + validateSendRequest(req); // ===== [1] 발신자 조회 ===== log.info("[발신자 조회] senderMemberId={} 조회 시작", senderMemberId); Member sender = memberRepository.findById(senderMemberId) @@ -172,6 +177,23 @@ public void sendMessage(Long senderMemberId, ChatSendMessageRequest req) { roomId, message.getId(), message.getMessageSeq(), senderMemberId, req.messageType() ); + ChatMessageAttachment savedAttachment = null; + if (req.messageType() == MessageType.ATTACHMENT) { + + ChatAttachmentRequest a = req.attachment(); + + ChatMessageAttachment attachment = ChatMessageAttachment.builder() + .message(message) + .attachmentType(a.attachmentType()) + .objectKey(a.objectKey()) + .mimeType(a.mimeType()) + .sizeBytes(a.sizeBytes()) + .durationMs(a.durationMs()) + .status(AttachmentStatus.READY) // 초기 상태는 READY + .build(); + + savedAttachment = chatMessageAttachmentRepository.save(attachment); + } // ===== [5] 내 메시지 자동 읽음 처리 ===== myMembership.markRead(nextSeq, message.getId()); log.info("[읽음 처리] senderMemberId={} 가 보낸 메시지 자동 읽음 처리. roomId={}, readSeq={}, readMessageId={}", @@ -210,6 +232,19 @@ public void sendMessage(Long senderMemberId, ChatSendMessageRequest req) { log.info("[상대 조회] roomId={}, senderMemberId={}, opponentId={}", roomId, senderMemberId, opponentId ); + ChatMessageItemDto.AttachmentMeta meta = null; + + if (savedAttachment != null) { + meta = ChatMessageItemDto.AttachmentMeta.builder() + .attachmentType(savedAttachment.getAttachmentType()) + .objectKey(savedAttachment.getObjectKey()) + .mimeType(savedAttachment.getMimeType()) + .sizeBytes(savedAttachment.getSizeBytes()) + .status(savedAttachment.getStatus()) + .durationMs(savedAttachment.getDurationMs()) + .build(); + } + // ===== [8] 방 토픽 발행 ===== ChatRoomMessageDto roomMessageDto = ChatRoomMessageDto.builder() @@ -220,7 +255,7 @@ public void sendMessage(Long senderMemberId, ChatSendMessageRequest req) { .messageType(req.messageType()) .content(req.content()) .createdAt(now) - .attachment(null) + .attachment(meta) .build(); String roomTopicDest = "/topic/chat/rooms/" + roomId; @@ -379,4 +414,30 @@ public ChatMessageSliceResponse getRoomMessages(Long memberId, Long roomId, Inte .nextCursorSeq(nextCursor) .build(); } + + private void validateSendRequest(ChatSendMessageRequest req) { + if (req.messageType() == MessageType.TEXT) { + if (req.content() == null || req.content().isBlank()) { + throw new BusinessException(ChatErrorCode.INVALID_MESSAGE_CONTENT); + } + if (req.attachment() != null) { + throw new BusinessException(ChatErrorCode.INVALID_ATTACHMENT_FOR_TEXT); + } + return; + } + + // ATTACHMENT + if (req.attachment() == null) { + throw new BusinessException(ChatErrorCode.INVALID_ATTACHMENT_REQUIRED); + } + if (req.attachment().objectKey() == null || req.attachment().objectKey().isBlank()) { + throw new BusinessException(ChatErrorCode.INVALID_ATTACHMENT_OBJECT_KEY); + } + + // 보안/검증: objectKey 접두어 제한 추천 + // 예: attachments/ 로 시작하는 것만 허용 + if (!req.attachment().objectKey().startsWith("attachments/")) { + throw new BusinessException(ChatErrorCode.INVALID_ATTACHMENT_OBJECT_KEY); + } + } }