Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,37 +3,35 @@
import com.back.domain.chat.dto.ChatPageResponse;
import com.back.domain.chat.service.ChatService;
import com.back.global.common.dto.RsData;
import com.back.global.security.CustomUserDetails;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;

import java.time.LocalDateTime;
import java.util.Map;

@RestController
@RequestMapping("/api")
@RequiredArgsConstructor
@RequestMapping("/api")
@Tag(name = "Chat API", description = "채팅 메시지 조회 관련 API")
public class ChatApiController {

private final ChatService chatService;

// 방 채팅 메시지 조회 (페이징, 특정 시간 이전 메시지)
@GetMapping("/rooms/{roomId}/messages")
@Operation(summary = "채팅방 메시지 목록 조회", description = "특정 채팅방의 이전 메시지 기록을 페이징하여 조회합니다.")
public ResponseEntity<RsData<ChatPageResponse>> getRoomChatMessages(
@PathVariable Long roomId,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "50") int size,
@RequestParam(defaultValue = "20") int size,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime before,
@RequestHeader("Authorization") String authorization) {

// size 최대값 제한 (임시: max 100)
if (size > 100) {
size = 100;
}

// TODO: JWT 토큰에서 사용자 정보 추출 및 권한 확인
@AuthenticationPrincipal CustomUserDetails userDetails) {

ChatPageResponse chatHistory = chatService.getRoomChatHistory(roomId, page, size, before);

Expand All @@ -42,28 +40,4 @@ public ResponseEntity<RsData<ChatPageResponse>> getRoomChatMessages(
.body(RsData.success("채팅 기록 조회 성공", chatHistory));
}

// 방 채팅 메시지 삭제
@DeleteMapping("/rooms/{roomId}/messages/{messageId}")
public ResponseEntity<RsData<Map<String, Object>>> deleteRoomMessage(
@PathVariable Long roomId,
@PathVariable Long messageId,
@RequestHeader("Authorization") String authorization) {

// TODO: JWT 토큰에서 사용자 정보 추출

// 임시로 하드코딩 (테스트용)
Long currentUserId = 1L;

// 메시지 삭제 로직 실행
chatService.deleteRoomMessage(roomId, messageId, currentUserId);

Map<String, Object> responseData = Map.of(
"messageId", messageId,
"deletedAt", LocalDateTime.now()
);

return ResponseEntity
.status(HttpStatus.OK)
.body(RsData.success("메시지 삭제 성공", responseData));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,23 @@

import com.back.domain.studyroom.entity.RoomChatMessage;
import com.back.domain.chat.dto.ChatMessageDto;
import com.back.global.security.CustomUserDetails;
import com.back.global.websocket.dto.WebSocketErrorResponse;
import com.back.domain.chat.service.ChatService;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.messaging.handler.annotation.DestinationVariable;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Controller;

import java.security.Principal;

@Controller
@RequiredArgsConstructor
@Tag(name = "Chat WebSocket", description = "STOMP를 이용한 실시간 채팅 WebSocket 컨트롤러 (Swagger에서 직접 테스트 불가)")
public class ChatWebSocketController {

private final ChatService chatService;
Expand All @@ -25,18 +31,24 @@ public class ChatWebSocketController {
* @param roomId 스터디룸 ID
* @param chatMessage 채팅 메시지 (content, messageType, attachmentId)
* @param headerAccessor WebSocket 헤더 정보
* @param principal 인증된 사용자 정보
*/
@MessageMapping("/chat/room/{roomId}")
public void handleRoomChat(@DestinationVariable Long roomId,
ChatMessageDto chatMessage,
SimpMessageHeaderAccessor headerAccessor) {
SimpMessageHeaderAccessor headerAccessor,
Principal principal) {

try {
// TODO: WebSocket 세션에서 사용자 정보 추출
// WebSocket에서 인증된 사용자 정보 추출
CustomUserDetails userDetails = extractUserDetails(principal);
if (userDetails == null) {
sendErrorToUser(headerAccessor.getSessionId(), "WS_UNAUTHORIZED", "인증이 필요합니다");
return;
}

// 임시 하드코딩 (나중에 JWT 인증으로 교체)
Long currentUserId = 1L;
String currentUserNickname = "테스트사용자";
Long currentUserId = userDetails.getUserId();
String currentUserNickname = userDetails.getUsername();

// 메시지 정보 보완
chatMessage.setRoomId(roomId);
Expand Down Expand Up @@ -74,4 +86,22 @@ public void handleRoomChat(@DestinationVariable Long roomId,
messagingTemplate.convertAndSendToUser(sessionId, "/queue/errors", errorResponse);
}
}

// WebSocket Principal에서 CustomUserDetails 추출
private CustomUserDetails extractUserDetails(Principal principal) {
if (principal instanceof Authentication auth) {
Object principalObj = auth.getPrincipal();
if (principalObj instanceof CustomUserDetails userDetails) {
return userDetails;
}
}
return null;
}

// 특정 사용자에게 에러 메시지 전송
private void sendErrorToUser(String sessionId, String errorCode, String errorMessage) {
WebSocketErrorResponse errorResponse = WebSocketErrorResponse.create(errorCode, errorMessage);
messagingTemplate.convertAndSendToUser(sessionId, "/queue/errors", errorResponse);
}

}
2 changes: 2 additions & 0 deletions src/main/java/com/back/domain/chat/dto/ChatPageResponse.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public class ChatPageResponse {

private List<ChatMessageDto> content;
private PageableDto pageable;
private long totalElements;

// 페이징 정보 DTO
@Data
Expand All @@ -36,6 +37,7 @@ public static ChatPageResponse from(org.springframework.data.domain.Page<ChatMes
.size(page.getSize())
.hasNext(page.hasNext())
.build())
.totalElements(page.getTotalElements())
.build();
}
}
44 changes: 20 additions & 24 deletions src/main/java/com/back/domain/chat/service/ChatService.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import com.back.domain.chat.dto.ChatPageResponse;
import com.back.global.exception.CustomException;
import com.back.global.exception.ErrorCode;
import com.back.global.security.CurrentUser;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
Expand All @@ -27,6 +28,11 @@ public class ChatService {
private final RoomChatMessageRepository roomChatMessageRepository;
private final RoomRepository roomRepository;
private final UserRepository userRepository;
private final CurrentUser currentUser;

// 페이징 설정 상수
private static final int DEFAULT_PAGE_SIZE = 20;
private static final int MAX_PAGE_SIZE = 100;

// 방 채팅 메시지 저장
@Transactional
Expand Down Expand Up @@ -54,22 +60,32 @@ public ChatPageResponse getRoomChatHistory(Long roomId, int page, int size, Loca
roomRepository.findById(roomId)
.orElseThrow(() -> new CustomException(ErrorCode.ROOM_NOT_FOUND));

Pageable pageable = PageRequest.of(page, size);
// size 값 검증 및 제한
int validatedSize = validateAndLimitPageSize(size);

Pageable pageable = PageRequest.of(page, validatedSize);

// before 파라미터가 있으면 해당 시점 이전 메시지만 조회
Page<RoomChatMessage> messagesPage;
if (before != null) {
// TODO: before 조건 추가한 Repository 메서드 필요
messagesPage = roomChatMessageRepository.findByRoomIdOrderByCreatedAtDesc(roomId, pageable);
messagesPage = roomChatMessageRepository.findMessagesByRoomIdBefore(roomId, before, pageable);
} else {
messagesPage = roomChatMessageRepository.findByRoomIdOrderByCreatedAtDesc(roomId, pageable);
messagesPage = roomChatMessageRepository.findMessagesByRoomId(roomId, pageable);
}

Page<ChatMessageDto> dtoPage = messagesPage.map(this::convertToDto);

return ChatPageResponse.from(dtoPage);
}

// size 값 검증 및 최대값 제한
private int validateAndLimitPageSize(int size) {
if (size <= 0) {
return DEFAULT_PAGE_SIZE; // 0 이하면 기본값 사용
}
return Math.min(size, MAX_PAGE_SIZE); // 최대값 제한
}

// 메시지 엔티티를 DTO로 변환
private ChatMessageDto convertToDto(RoomChatMessage message) {
return ChatMessageDto.builder()
Expand All @@ -85,24 +101,4 @@ private ChatMessageDto convertToDto(RoomChatMessage message) {
.build();
}

// 방 채팅 메시지 삭제
@Transactional
public void deleteRoomMessage(Long roomId, Long messageId, Long currentUserId) {
// 메시지 존재 여부 확인
RoomChatMessage message = roomChatMessageRepository.findById(messageId)
.orElseThrow(() -> new CustomException(ErrorCode.MESSAGE_NOT_FOUND));

// 방 ID 검증
if (!message.getRoom().getId().equals(roomId)) {
throw new CustomException(ErrorCode.BAD_REQUEST);
}

// 작성자 권한 확인
if (!message.getUser().getId().equals(currentUserId)) {
throw new CustomException(ErrorCode.MESSAGE_FORBIDDEN);
}

// 메시지 삭제
roomChatMessageRepository.delete(message);
}
}
19 changes: 18 additions & 1 deletion src/main/java/com/back/domain/studyroom/entity/Room.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,20 @@
import com.back.domain.user.entity.User;
import com.back.global.entity.BaseEntity;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;

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

@Entity
@NoArgsConstructor
@Getter
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
public class Room extends BaseEntity {
private String title;
private String description;
Expand All @@ -25,31 +30,43 @@ public class Room extends BaseEntity {
private boolean allowScreenShare;

// 방 상태
@Builder.Default
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private RoomStatus status = RoomStatus.WAITING;

// 현재 참여자
@Builder.Default
@Column(nullable = false)
private int currentParticipants = 0;

// 방장
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "created_by")
private User createdBy;

// 테마
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "theme_id")
private RoomTheme theme;

// 연관관계 설정
@Builder.Default
@OneToMany(mappedBy = "room", cascade = CascadeType.ALL, orphanRemoval = true)
private List<RoomMember> roomMembers = new ArrayList<>();

// 채팅 메시지
@Builder.Default
@OneToMany(mappedBy = "room", cascade = CascadeType.ALL, orphanRemoval = true)
private List<RoomChatMessage> roomChatMessages = new ArrayList<>();

// 참가자 기록
@Builder.Default
@OneToMany(mappedBy = "room", cascade = CascadeType.ALL, orphanRemoval = true)
private List<RoomParticipantHistory> roomParticipantHistories = new ArrayList<>();

// 스터디 기록
@Builder.Default
@OneToMany(mappedBy = "room", cascade = CascadeType.ALL, orphanRemoval = true)
public List<StudyRecord> studyRecords = new ArrayList<>();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,17 @@
import jakarta.persistence.FetchType;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;

@Entity
@Getter
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
public class RoomChatMessage extends BaseEntity {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "room_id")
Expand All @@ -23,10 +28,4 @@ public class RoomChatMessage extends BaseEntity {

private String content;

// 채팅 메세지 생성자
public RoomChatMessage(Room room, User user, String content) {
this.room = room;
this.user = user;
this.content = content;
}
}
Original file line number Diff line number Diff line change
@@ -1,36 +1,9 @@
package com.back.domain.studyroom.repository;

import com.back.domain.studyroom.entity.RoomChatMessage;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

import java.time.LocalDateTime;
import java.util.List;

@Repository
public interface RoomChatMessageRepository extends JpaRepository<RoomChatMessage, Long> {

// 방별 페이징된 채팅 메시지 조회 (무한 스크롤용)
@Query("SELECT m FROM RoomChatMessage m " +
"WHERE m.room.id = :roomId " +
"ORDER BY m.createdAt DESC")
Page<RoomChatMessage> findByRoomIdOrderByCreatedAtDesc(@Param("roomId") Long roomId, Pageable pageable);

// 특정 타임스탬프 이후의 메시지 조회 (실시간 업데이트용)
@Query("SELECT m FROM RoomChatMessage m " +
"WHERE m.room.id = :roomId " +
"AND m.createdAt > :timestamp " +
"ORDER BY m.createdAt ASC")
List<RoomChatMessage> findByRoomIdAfterTimestamp(@Param("roomId") Long roomId,
@Param("timestamp") LocalDateTime timestamp);

// 방별 최근 20개 메시지 조회
List<RoomChatMessage> findTop20ByRoomIdOrderByCreatedAtDesc(Long roomId);

// 방별 전체 메시지 수 조회
long countByRoomId(Long roomId);
}
public interface RoomChatMessageRepository extends JpaRepository<RoomChatMessage, Long>, RoomChatMessageRepositoryCustom {
}
Loading