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
1 change: 0 additions & 1 deletion .env.default

This file was deleted.

33 changes: 26 additions & 7 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -25,25 +25,44 @@ repositories {
}

dependencies {
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
// Spring
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-websocket")
implementation("org.springframework.boot:spring-boot-starter-security")
testImplementation("org.springframework.security:spring-security-test")
compileOnly("org.projectlombok:lombok")
developmentOnly("org.springframework.boot:spring-boot-devtools")

// Database & JPA
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
runtimeOnly("com.h2database:h2")
runtimeOnly("com.mysql:mysql-connector-j")

// QueryDSL
implementation("com.querydsl:querydsl-jpa:5.0.0:jakarta")
annotationProcessor("com.querydsl:querydsl-apt:5.0.0:jakarta")
annotationProcessor("jakarta.persistence:jakarta.persistence-api")
annotationProcessor("jakarta.annotation:jakarta.annotation-api")

// Security
implementation("org.springframework.boot:spring-boot-starter-security")

// Development Tools
compileOnly("org.projectlombok:lombok")
annotationProcessor("org.projectlombok:lombok")
testImplementation("org.springframework.boot:spring-boot-starter-test")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
developmentOnly("org.springframework.boot:spring-boot-devtools")

// Swagger
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.13")

// Env
implementation ("io.github.cdimascio:dotenv-java:3.0.0")

// JWT
implementation("io.jsonwebtoken:jjwt-api:0.12.6")
runtimeOnly("io.jsonwebtoken:jjwt-impl:0.12.6")
runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.12.6")

// Test
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.springframework.security:spring-security-test")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}

tasks.withType<Test> {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package com.back.domain.chat.controller;

import com.back.domain.chat.dto.ChatPageResponse;
import com.back.domain.chat.service.ChatService;
import com.back.global.common.dto.RsData;
import lombok.RequiredArgsConstructor;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

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

@RestController
@RequestMapping("/api")
@RequiredArgsConstructor
public class ChatApiController {

private final ChatService chatService;

// 방 채팅 메시지 조회 (페이징, 특정 시간 이전 메시지)
@GetMapping("/rooms/{roomId}/messages")
public ResponseEntity<RsData<ChatPageResponse>> getRoomChatMessages(
@PathVariable Long roomId,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "50") 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 토큰에서 사용자 정보 추출 및 권한 확인

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

return ResponseEntity
.status(HttpStatus.OK)
.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
@@ -0,0 +1,77 @@
package com.back.domain.chat.controller;

import com.back.domain.studyroom.entity.RoomChatMessage;
import com.back.domain.chat.dto.ChatMessageDto;
import com.back.global.websocket.dto.WebSocketErrorResponse;
import com.back.domain.chat.service.ChatService;
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.stereotype.Controller;

@Controller
@RequiredArgsConstructor
public class ChatWebSocketController {

private final ChatService chatService;
private final SimpMessagingTemplate messagingTemplate;

/**
* 방 채팅 메시지 처리
* 클라이언트가 /app/chat/room/{roomId}로 메시지 전송 시 호출
*
* @param roomId 스터디룸 ID
* @param chatMessage 채팅 메시지 (content, messageType, attachmentId)
* @param headerAccessor WebSocket 헤더 정보
*/
@MessageMapping("/chat/room/{roomId}")
public void handleRoomChat(@DestinationVariable Long roomId,
ChatMessageDto chatMessage,
SimpMessageHeaderAccessor headerAccessor) {

try {
// TODO: WebSocket 세션에서 사용자 정보 추출

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

// 메시지 정보 보완
chatMessage.setRoomId(roomId);
chatMessage.setUserId(currentUserId);
chatMessage.setNickname(currentUserNickname);

// DB에 메시지 저장
RoomChatMessage savedMessage = chatService.saveRoomChatMessage(chatMessage);

// 저장된 메시지 정보로 응답 DTO 생성
ChatMessageDto responseMessage = ChatMessageDto.builder()
.messageId(savedMessage.getId())
.roomId(roomId)
.userId(savedMessage.getUser().getId())
.nickname(savedMessage.getUser().getNickname())
.profileImageUrl(savedMessage.getUser().getProfileImageUrl())
.content(savedMessage.getContent())
.messageType(chatMessage.getMessageType())
.attachment(null) // 텍스트 채팅에서는 null
.createdAt(savedMessage.getCreatedAt())
.build();

// 해당 방의 모든 구독자에게 브로드캐스트
messagingTemplate.convertAndSend("/topic/room/" + roomId, responseMessage);

} catch (Exception e) {
// 에러 응답을 해당 사용자에게만 전송
WebSocketErrorResponse errorResponse = WebSocketErrorResponse.create(
"WS_ROOM_NOT_FOUND",
"존재하지 않는 방입니다"
);

// 에러를 발생시킨 사용자에게만 전송
String sessionId = headerAccessor.getSessionId();
messagingTemplate.convertAndSendToUser(sessionId, "/queue/errors", errorResponse);
}
}
}
51 changes: 51 additions & 0 deletions src/main/java/com/back/domain/chat/dto/ChatMessageDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.back.domain.chat.dto;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ChatMessageDto {

// WebSocket Request
private String content;
private String messageType;
private Long attachmentId;

// WebSocket Response
private Long messageId;
private Long roomId;
private Long userId;
private String nickname;
private String profileImageUrl;
private AttachmentDto attachment;
private LocalDateTime createdAt;

// 첨부파일 DTO (나중에 파일 기능 구현 시 사용)
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class AttachmentDto {
private Long id;
private String originalName;
private String url;
private Long size;
private String mimeType;
}

// 텍스트 채팅 요청 생성 헬퍼
public static ChatMessageDto createRequest(String content, String messageType) {
return ChatMessageDto.builder()
.content(content)
.messageType(messageType)
.attachmentId(null) // 텍스트 채팅에서는 null
.build();
}
}
41 changes: 41 additions & 0 deletions src/main/java/com/back/domain/chat/dto/ChatPageResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.back.domain.chat.dto;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.List;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ChatPageResponse {

private List<ChatMessageDto> content;
private PageableDto pageable;

// 페이징 정보 DTO
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class PageableDto {
private int page;
private int size;
private boolean hasNext;
}

// Page<ChatMessageDto> -> ChatPageResponse 변환 헬퍼
public static ChatPageResponse from(org.springframework.data.domain.Page<ChatMessageDto> page) {
return ChatPageResponse.builder()
.content(page.getContent())
.pageable(PageableDto.builder()
.page(page.getNumber())
.size(page.getSize())
.hasNext(page.hasNext())
.build())
.build();
}
}
Loading