Skip to content

Commit 62d21ea

Browse files
authored
Merge pull request #47 from prgrms-web-devcourse-final-project/Feat/37
Feat: 실시간 스터디룸 채팅 기능 구현 (#37)
2 parents 95b305e + 49f1607 commit 62d21ea

21 files changed

+595
-11
lines changed

.env.default

Lines changed: 0 additions & 1 deletion
This file was deleted.

build.gradle.kts

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,25 +25,44 @@ repositories {
2525
}
2626

2727
dependencies {
28-
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
28+
// Spring
2929
implementation("org.springframework.boot:spring-boot-starter-web")
3030
implementation("org.springframework.boot:spring-boot-starter-websocket")
31-
implementation("org.springframework.boot:spring-boot-starter-security")
32-
testImplementation("org.springframework.security:spring-security-test")
33-
compileOnly("org.projectlombok:lombok")
34-
developmentOnly("org.springframework.boot:spring-boot-devtools")
31+
32+
// Database & JPA
33+
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
3534
runtimeOnly("com.h2database:h2")
3635
runtimeOnly("com.mysql:mysql-connector-j")
36+
37+
// QueryDSL
38+
implementation("com.querydsl:querydsl-jpa:5.0.0:jakarta")
39+
annotationProcessor("com.querydsl:querydsl-apt:5.0.0:jakarta")
40+
annotationProcessor("jakarta.persistence:jakarta.persistence-api")
41+
annotationProcessor("jakarta.annotation:jakarta.annotation-api")
42+
43+
// Security
44+
implementation("org.springframework.boot:spring-boot-starter-security")
45+
46+
// Development Tools
47+
compileOnly("org.projectlombok:lombok")
3748
annotationProcessor("org.projectlombok:lombok")
38-
testImplementation("org.springframework.boot:spring-boot-starter-test")
39-
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
49+
developmentOnly("org.springframework.boot:spring-boot-devtools")
50+
51+
// Swagger
4052
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.13")
53+
54+
// Env
4155
implementation ("io.github.cdimascio:dotenv-java:3.0.0")
4256

4357
// JWT
4458
implementation("io.jsonwebtoken:jjwt-api:0.12.6")
4559
runtimeOnly("io.jsonwebtoken:jjwt-impl:0.12.6")
4660
runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.12.6")
61+
62+
// Test
63+
testImplementation("org.springframework.boot:spring-boot-starter-test")
64+
testImplementation("org.springframework.security:spring-security-test")
65+
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
4766
}
4867

4968
tasks.withType<Test> {
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package com.back.domain.chat.controller;
2+
3+
import com.back.domain.chat.dto.ChatPageResponse;
4+
import com.back.domain.chat.service.ChatService;
5+
import com.back.global.common.dto.RsData;
6+
import lombok.RequiredArgsConstructor;
7+
import org.springframework.format.annotation.DateTimeFormat;
8+
import org.springframework.http.HttpStatus;
9+
import org.springframework.http.ResponseEntity;
10+
import org.springframework.web.bind.annotation.*;
11+
12+
import java.time.LocalDateTime;
13+
import java.util.Map;
14+
15+
@RestController
16+
@RequestMapping("/api")
17+
@RequiredArgsConstructor
18+
public class ChatApiController {
19+
20+
private final ChatService chatService;
21+
22+
// 방 채팅 메시지 조회 (페이징, 특정 시간 이전 메시지)
23+
@GetMapping("/rooms/{roomId}/messages")
24+
public ResponseEntity<RsData<ChatPageResponse>> getRoomChatMessages(
25+
@PathVariable Long roomId,
26+
@RequestParam(defaultValue = "0") int page,
27+
@RequestParam(defaultValue = "50") int size,
28+
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime before,
29+
@RequestHeader("Authorization") String authorization) {
30+
31+
// size 최대값 제한 (임시: max 100)
32+
if (size > 100) {
33+
size = 100;
34+
}
35+
36+
// TODO: JWT 토큰에서 사용자 정보 추출 및 권한 확인
37+
38+
ChatPageResponse chatHistory = chatService.getRoomChatHistory(roomId, page, size, before);
39+
40+
return ResponseEntity
41+
.status(HttpStatus.OK)
42+
.body(RsData.success("채팅 기록 조회 성공", chatHistory));
43+
}
44+
45+
// 방 채팅 메시지 삭제
46+
@DeleteMapping("/rooms/{roomId}/messages/{messageId}")
47+
public ResponseEntity<RsData<Map<String, Object>>> deleteRoomMessage(
48+
@PathVariable Long roomId,
49+
@PathVariable Long messageId,
50+
@RequestHeader("Authorization") String authorization) {
51+
52+
// TODO: JWT 토큰에서 사용자 정보 추출
53+
54+
// 임시로 하드코딩 (테스트용)
55+
Long currentUserId = 1L;
56+
57+
// 메시지 삭제 로직 실행
58+
chatService.deleteRoomMessage(roomId, messageId, currentUserId);
59+
60+
Map<String, Object> responseData = Map.of(
61+
"messageId", messageId,
62+
"deletedAt", LocalDateTime.now()
63+
);
64+
65+
return ResponseEntity
66+
.status(HttpStatus.OK)
67+
.body(RsData.success("메시지 삭제 성공", responseData));
68+
}
69+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package com.back.domain.chat.controller;
2+
3+
import com.back.domain.studyroom.entity.RoomChatMessage;
4+
import com.back.domain.chat.dto.ChatMessageDto;
5+
import com.back.global.websocket.dto.WebSocketErrorResponse;
6+
import com.back.domain.chat.service.ChatService;
7+
import lombok.RequiredArgsConstructor;
8+
import org.springframework.messaging.handler.annotation.DestinationVariable;
9+
import org.springframework.messaging.handler.annotation.MessageMapping;
10+
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
11+
import org.springframework.messaging.simp.SimpMessagingTemplate;
12+
import org.springframework.stereotype.Controller;
13+
14+
@Controller
15+
@RequiredArgsConstructor
16+
public class ChatWebSocketController {
17+
18+
private final ChatService chatService;
19+
private final SimpMessagingTemplate messagingTemplate;
20+
21+
/**
22+
* 방 채팅 메시지 처리
23+
* 클라이언트가 /app/chat/room/{roomId}로 메시지 전송 시 호출
24+
*
25+
* @param roomId 스터디룸 ID
26+
* @param chatMessage 채팅 메시지 (content, messageType, attachmentId)
27+
* @param headerAccessor WebSocket 헤더 정보
28+
*/
29+
@MessageMapping("/chat/room/{roomId}")
30+
public void handleRoomChat(@DestinationVariable Long roomId,
31+
ChatMessageDto chatMessage,
32+
SimpMessageHeaderAccessor headerAccessor) {
33+
34+
try {
35+
// TODO: WebSocket 세션에서 사용자 정보 추출
36+
37+
// 임시 하드코딩 (나중에 JWT 인증으로 교체)
38+
Long currentUserId = 1L;
39+
String currentUserNickname = "테스트사용자";
40+
41+
// 메시지 정보 보완
42+
chatMessage.setRoomId(roomId);
43+
chatMessage.setUserId(currentUserId);
44+
chatMessage.setNickname(currentUserNickname);
45+
46+
// DB에 메시지 저장
47+
RoomChatMessage savedMessage = chatService.saveRoomChatMessage(chatMessage);
48+
49+
// 저장된 메시지 정보로 응답 DTO 생성
50+
ChatMessageDto responseMessage = ChatMessageDto.builder()
51+
.messageId(savedMessage.getId())
52+
.roomId(roomId)
53+
.userId(savedMessage.getUser().getId())
54+
.nickname(savedMessage.getUser().getNickname())
55+
.profileImageUrl(savedMessage.getUser().getProfileImageUrl())
56+
.content(savedMessage.getContent())
57+
.messageType(chatMessage.getMessageType())
58+
.attachment(null) // 텍스트 채팅에서는 null
59+
.createdAt(savedMessage.getCreatedAt())
60+
.build();
61+
62+
// 해당 방의 모든 구독자에게 브로드캐스트
63+
messagingTemplate.convertAndSend("/topic/room/" + roomId, responseMessage);
64+
65+
} catch (Exception e) {
66+
// 에러 응답을 해당 사용자에게만 전송
67+
WebSocketErrorResponse errorResponse = WebSocketErrorResponse.create(
68+
"WS_ROOM_NOT_FOUND",
69+
"존재하지 않는 방입니다"
70+
);
71+
72+
// 에러를 발생시킨 사용자에게만 전송
73+
String sessionId = headerAccessor.getSessionId();
74+
messagingTemplate.convertAndSendToUser(sessionId, "/queue/errors", errorResponse);
75+
}
76+
}
77+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package com.back.domain.chat.dto;
2+
3+
import lombok.AllArgsConstructor;
4+
import lombok.Builder;
5+
import lombok.Data;
6+
import lombok.NoArgsConstructor;
7+
8+
import java.time.LocalDateTime;
9+
10+
@Data
11+
@Builder
12+
@NoArgsConstructor
13+
@AllArgsConstructor
14+
public class ChatMessageDto {
15+
16+
// WebSocket Request
17+
private String content;
18+
private String messageType;
19+
private Long attachmentId;
20+
21+
// WebSocket Response
22+
private Long messageId;
23+
private Long roomId;
24+
private Long userId;
25+
private String nickname;
26+
private String profileImageUrl;
27+
private AttachmentDto attachment;
28+
private LocalDateTime createdAt;
29+
30+
// 첨부파일 DTO (나중에 파일 기능 구현 시 사용)
31+
@Data
32+
@Builder
33+
@NoArgsConstructor
34+
@AllArgsConstructor
35+
public static class AttachmentDto {
36+
private Long id;
37+
private String originalName;
38+
private String url;
39+
private Long size;
40+
private String mimeType;
41+
}
42+
43+
// 텍스트 채팅 요청 생성 헬퍼
44+
public static ChatMessageDto createRequest(String content, String messageType) {
45+
return ChatMessageDto.builder()
46+
.content(content)
47+
.messageType(messageType)
48+
.attachmentId(null) // 텍스트 채팅에서는 null
49+
.build();
50+
}
51+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package com.back.domain.chat.dto;
2+
3+
import lombok.AllArgsConstructor;
4+
import lombok.Builder;
5+
import lombok.Data;
6+
import lombok.NoArgsConstructor;
7+
8+
import java.util.List;
9+
10+
@Data
11+
@Builder
12+
@NoArgsConstructor
13+
@AllArgsConstructor
14+
public class ChatPageResponse {
15+
16+
private List<ChatMessageDto> content;
17+
private PageableDto pageable;
18+
19+
// 페이징 정보 DTO
20+
@Data
21+
@Builder
22+
@NoArgsConstructor
23+
@AllArgsConstructor
24+
public static class PageableDto {
25+
private int page;
26+
private int size;
27+
private boolean hasNext;
28+
}
29+
30+
// Page<ChatMessageDto> -> ChatPageResponse 변환 헬퍼
31+
public static ChatPageResponse from(org.springframework.data.domain.Page<ChatMessageDto> page) {
32+
return ChatPageResponse.builder()
33+
.content(page.getContent())
34+
.pageable(PageableDto.builder()
35+
.page(page.getNumber())
36+
.size(page.getSize())
37+
.hasNext(page.hasNext())
38+
.build())
39+
.build();
40+
}
41+
}

0 commit comments

Comments
 (0)