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 @@ -4,17 +4,22 @@
import com.deepdirect.deepwebide_be.chat.dto.response.ChatMessageBroadcast;
import com.deepdirect.deepwebide_be.chat.service.ChatMessageWriteService;
import com.deepdirect.deepwebide_be.chat.util.RedisPublisher;
import com.deepdirect.deepwebide_be.member.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
import org.springframework.stereotype.Controller;


@Slf4j
@Controller
@RequiredArgsConstructor
public class ChatWebSocketController {

private final ChatMessageWriteService chatMessageWriteService;
private final RedisPublisher redisPublisher;
private final UserRepository userRepository;

@MessageMapping("/repositories/{repositoryId}/chat/send")
public void sendMessage(
Expand All @@ -25,6 +30,10 @@ public void sendMessage(
// 1. WebSocket 세션에서 userId 추출
Long userId = (Long) headerAccessor.getSessionAttributes().get("userId");

String username = userRepository.findById(userId).get().getUsername();
log.info("WebSocket 메시지 전송: userId={}, username={}", userId, username);

System.out.println(request.getMessage());
// 2. 메시지 저장 + DTO 응답 변환
ChatMessageBroadcast broadcast = chatMessageWriteService.saveChatMessage(userId, request);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
import com.deepdirect.deepwebide_be.chat.domain.ChatMessageType;
import com.deepdirect.deepwebide_be.member.domain.User;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Getter;
import lombok.*;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.deepdirect.deepwebide_be.chat.util;

import com.deepdirect.deepwebide_be.chat.dto.response.ChatMessageBroadcast;
import com.deepdirect.deepwebide_be.chat.dto.response.ChatSystemMessageResponse;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.sentry.Sentry;
import lombok.RequiredArgsConstructor;
Expand All @@ -24,28 +26,35 @@ public class RedisSubscriber implements MessageListener {
public void onMessage(Message message, byte[] pattern) {
try {
String raw = new String(message.getBody(), StandardCharsets.UTF_8);
ChatMessageBroadcast broadcast = objectMapper.readValue(raw, ChatMessageBroadcast.class);

// isMine은 false로 변경 (다른 사람들에게 보내는 메시지)
ChatMessageBroadcast response = ChatMessageBroadcast.builder()
.type(broadcast.getType())
.messageId(broadcast.getMessageId())
.senderId(broadcast.getSenderId())
.senderNickname(broadcast.getSenderNickname())
.senderProfileImageUrl(broadcast.getSenderProfileImageUrl())
.message(broadcast.getMessage())
.sentAt(broadcast.getSentAt())
.isMine(false)
.build();
JsonNode root = objectMapper.readTree(raw);

String type = root.get("type").asText();
String topic = new String(message.getChannel(), StandardCharsets.UTF_8);
Long repositoryId = Long.parseLong(topic.split(":")[1]);

// 구독 중인 사용자들에게 메시지 전송
messagingTemplate.convertAndSend(
"/sub/repositories/" + repositoryId + "/chat",
response
);
log.info("📩 Redis 메시지 도착! raw: {}", raw);

if ("CHAT".equals(type)) {
ChatMessageBroadcast broadcast = objectMapper.treeToValue(root, ChatMessageBroadcast.class);
ChatMessageBroadcast response = ChatMessageBroadcast.builder()
.type(broadcast.getType())
.messageId(broadcast.getMessageId())
.senderId(broadcast.getSenderId())
.senderNickname(broadcast.getSenderNickname())
.senderProfileImageUrl(broadcast.getSenderProfileImageUrl())
.message(broadcast.getMessage())
.codeReference(broadcast.getCodeReference())
.sentAt(broadcast.getSentAt())
.isMine(false)
.build();

messagingTemplate.convertAndSend("/sub/repositories/" + repositoryId + "/chat", response);

} else {
// USER_JOINED, USER_LEFT
ChatSystemMessageResponse system = objectMapper.treeToValue(root, ChatSystemMessageResponse.class);
messagingTemplate.convertAndSend("/topic/repositories/" + repositoryId + "/chat", system);
}
} catch (Exception e) {
log.error("❌ RedisSubscriber: 메시지 처리 중 에러 발생", e);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,13 @@ public Message<?> preSend(Message<?> message, MessageChannel channel) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new GlobalException(ErrorCode.USER_NOT_FOUND));


String username = user.getUsername();
String nickname = user.getNickname();
log.info("WebSocket 연결: userId={}", userId);
log.info("WebSocket 연결: username={}", username);
log.info("WebSocket 연결: nickname={}", nickname);

accessor.setUser(new CustomUserDetails(user));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,14 @@ public class FileService {

public List<FileTreeNodeResponse> getFileTree(Long repositoryId, Long userId) {
// 1. 레포지토리/권한 체크
repositoryRepository.findByIdAndMemberOrOwner(repositoryId, userId)
repositoryRepository.findByIdAndDeletedAtIsNull(repositoryId)
.orElseThrow(() -> new GlobalException(ErrorCode.REPOSITORY_NOT_FOUND));

boolean hasAccess = repositoryRepository.findByIdAndMemberOrOwner(repositoryId, userId).isPresent();
if (!hasAccess) {
throw new GlobalException(ErrorCode.REPOSITORY_ACCESS_DENIED);
}

// 2. 모든 FileNode 조회 (1쿼리)
List<FileNode> allNodes = fileNodeRepository.findAllByRepositoryId(repositoryId);

Expand Down Expand Up @@ -75,9 +80,14 @@ public FileNodeResponse createFileOrFolder(Long repositoryId, Long userId, FileC
}

// 1. 레포 권한 체크
Repository repo = repositoryRepository.findByIdAndMemberOrOwner(repositoryId, userId)
Repository repo = repositoryRepository.findByIdAndDeletedAtIsNull(repositoryId)
.orElseThrow(() -> new GlobalException(ErrorCode.REPOSITORY_NOT_FOUND));

boolean hasAccess = repositoryRepository.findByIdAndMemberOrOwner(repositoryId, userId).isPresent();
if (!hasAccess) {
throw new GlobalException(ErrorCode.REPOSITORY_ACCESS_DENIED);
}

// 2. 부모 폴더 체크 (없으면 null)
FileNode parent = null;
String parentPath = "";
Expand Down Expand Up @@ -130,8 +140,14 @@ public FileNodeResponse createFileOrFolder(Long repositoryId, Long userId, FileC
@Transactional
public FileRenameResponse renameFileOrFolder(Long repositoryId, Long fileId, Long userId, String newFileName) {
// 1. 권한/레포 체크
repositoryRepository.findByIdAndMemberOrOwner(repositoryId, userId)
repositoryRepository.findByIdAndDeletedAtIsNull(repositoryId)
.orElseThrow(() -> new GlobalException(ErrorCode.REPOSITORY_NOT_FOUND));

boolean hasAccess = repositoryRepository.findByIdAndMemberOrOwner(repositoryId, userId).isPresent();
if (!hasAccess) {
throw new GlobalException(ErrorCode.REPOSITORY_ACCESS_DENIED);
}

FileNode fileNode = findFileNodeWithRepositoryCheck(repositoryId, fileId);

if (fileNode.getFileType() == FileType.FILE) {
Expand Down Expand Up @@ -170,8 +186,14 @@ private void updateChildPathsRecursively(FileNode parentNode) {

@Transactional
public void deleteFileOrFolder(Long repositoryId, Long fileId, Long userId) {
repositoryRepository.findByIdAndMemberOrOwner(repositoryId, userId)
repositoryRepository.findByIdAndDeletedAtIsNull(repositoryId)
.orElseThrow(() -> new GlobalException(ErrorCode.REPOSITORY_NOT_FOUND));

boolean hasAccess = repositoryRepository.findByIdAndMemberOrOwner(repositoryId, userId).isPresent();
if (!hasAccess) {
throw new GlobalException(ErrorCode.REPOSITORY_ACCESS_DENIED);
}

FileNode node = findFileNodeWithRepositoryCheck(repositoryId, fileId);

// (폴더일 경우) 하위 전체 삭제 (재귀)
Expand Down Expand Up @@ -210,8 +232,13 @@ public FileNodeResponse moveFileOrFolder(
throw new GlobalException(ErrorCode.PARENT_ID_REQUIRED);
}

repositoryRepository.findByIdAndMemberOrOwner(repositoryId, userId)
repositoryRepository.findByIdAndDeletedAtIsNull(repositoryId)
.orElseThrow(() -> new GlobalException(ErrorCode.REPOSITORY_NOT_FOUND));

boolean hasAccess = repositoryRepository.findByIdAndMemberOrOwner(repositoryId, userId).isPresent();
if (!hasAccess) {
throw new GlobalException(ErrorCode.REPOSITORY_ACCESS_DENIED);
}
FileNode fileNode = findFileNodeWithRepositoryCheck(repositoryId, fileId);

// 새 부모 폴더 체크
Expand Down Expand Up @@ -261,8 +288,13 @@ private boolean isDescendant(FileNode node, FileNode targetParent) {

@Transactional(readOnly = true)
public FileContentResponse getFileContent(Long repositoryId, Long fileId, Long userId) {
repositoryRepository.findByIdAndMemberOrOwner(repositoryId, userId)
repositoryRepository.findByIdAndDeletedAtIsNull(repositoryId)
.orElseThrow(() -> new GlobalException(ErrorCode.REPOSITORY_NOT_FOUND));

boolean hasAccess = repositoryRepository.findByIdAndMemberOrOwner(repositoryId, userId).isPresent();
if (!hasAccess) {
throw new GlobalException(ErrorCode.REPOSITORY_ACCESS_DENIED);
}
FileNode fileNode = findFileNodeWithRepositoryCheck(repositoryId, fileId);

if (fileNode.getFileType() == FileType.FOLDER) {
Expand Down Expand Up @@ -297,9 +329,14 @@ public FileContentResponse getFileContent(Long repositoryId, Long fileId, Long u
@Transactional
public FileContentSaveResponse saveFileContent(Long repositoryId, Long fileId, Long userId, String content) {
// 1. 레포 권한 및 존재 확인
Repository repo = repositoryRepository.findByIdAndMemberOrOwner(repositoryId, userId)
repositoryRepository.findByIdAndDeletedAtIsNull(repositoryId)
.orElseThrow(() -> new GlobalException(ErrorCode.REPOSITORY_NOT_FOUND));

boolean hasAccess = repositoryRepository.findByIdAndMemberOrOwner(repositoryId, userId).isPresent();
if (!hasAccess) {
throw new GlobalException(ErrorCode.REPOSITORY_ACCESS_DENIED);
}

// 2. 파일 노드 + 소속 레포 검증
FileNode fileNode = findFileNodeWithRepositoryCheck(repositoryId, fileId);

Expand Down Expand Up @@ -343,9 +380,14 @@ public FileNodeResponse uploadFile(Long repositoryId, Long userId, Long parentId


// 1. 권한/레포 체크
Repository repo = repositoryRepository.findByIdAndMemberOrOwner(repositoryId, userId)
Repository repo = repositoryRepository.findByIdAndDeletedAtIsNull(repositoryId)
.orElseThrow(() -> new GlobalException(ErrorCode.REPOSITORY_NOT_FOUND));

boolean hasAccess = repositoryRepository.findByIdAndMemberOrOwner(repositoryId, userId).isPresent();
if (!hasAccess) {
throw new GlobalException(ErrorCode.REPOSITORY_ACCESS_DENIED);
}

// 2. 부모 폴더 체크
FileNode parent = null;
String parentPath = "";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public void registerStompEndpoints(StompEndpointRegistry registry) {

@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
// registry.enableSimpleBroker("/topic"); // 구독 경로 (브라우저가 받을 때)
registry.enableSimpleBroker("/sub", "/topic"); // 구독 경로 (브라우저가 받을 때)
registry.setApplicationDestinationPrefixes("/app"); // 발행 경로 (브라우저가 보낼 때)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ public enum ErrorCode {
ENTRY_CODE_ACCESS_DENIED(HttpStatus.FORBIDDEN, "오너만 확인할 수 있습니다."),
ENTRY_CODE_REISSUE_DENIED(HttpStatus.FORBIDDEN, "해당 레포지토리의 소유자만 입장 코드를 재발급할 수 있습니다."),
NOT_OWNER_TO_KICK(HttpStatus.FORBIDDEN,"해당 레포의 소유자만 멤버를 강퇴할 수 있습니다."),
REPOSITORY_ACCESS_DENIED(HttpStatus.FORBIDDEN, "오너 및 멤버만 접근할 수 있습니다."),

// 404 NOT FOUND
USER_NOT_FOUND(HttpStatus.NOT_FOUND, "사용자를 찾을 수 없습니다."),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,13 @@ public class HistoryService {
@Transactional
public HistorySaveResponse saveHistory(Long repositoryId, Long userId, HistorySaveRequest request) {
// 1. 권한 체크 및 레포 조회
Repository repo = repositoryRepository.findByIdAndMemberOrOwner(repositoryId, userId)
Repository repo = repositoryRepository.findByIdAndDeletedAtIsNull(repositoryId)
.orElseThrow(() -> new GlobalException(ErrorCode.REPOSITORY_NOT_FOUND));

boolean hasAccess = repositoryRepository.findByIdAndMemberOrOwner(repositoryId, userId).isPresent();
if (!hasAccess) {
throw new GlobalException(ErrorCode.REPOSITORY_ACCESS_DENIED);
}
// 2. 현재 DB의 파일/폴더 전체 조회
List<FileNode> dbNodes = fileNodeRepository.findAllByRepositoryId(repositoryId);

Expand Down Expand Up @@ -97,9 +101,14 @@ public HistorySaveResponse saveHistory(Long repositoryId, Long userId, HistorySa
@Transactional(readOnly = true)
public HistoryDetailResponse getHistoryDetail(Long repositoryId, Long historyId, Long userId) {
// 1. 권한 체크 & 레포 확인
Repository repo = repositoryRepository.findByIdAndMemberOrOwner(repositoryId, userId)
Repository repo = repositoryRepository.findByIdAndDeletedAtIsNull(repositoryId)
.orElseThrow(() -> new GlobalException(ErrorCode.REPOSITORY_NOT_FOUND));

boolean hasAccess = repositoryRepository.findByIdAndMemberOrOwner(repositoryId, userId).isPresent();
if (!hasAccess) {
throw new GlobalException(ErrorCode.REPOSITORY_ACCESS_DENIED);
}

// 2. 히스토리 조회
History history = historyRepository.findById(historyId)
.orElseThrow(() -> new GlobalException(ErrorCode.HISTORY_NOT_FOUND));
Expand Down Expand Up @@ -130,9 +139,14 @@ public HistoryDetailResponse getHistoryDetail(Long repositoryId, Long historyId,
@Transactional(readOnly = true)
public List<HistoryListResponse> getHistories(Long repositoryId, Long userId) {
// 1. 권한 체크 & 레포 확인
Repository repo = repositoryRepository.findByIdAndMemberOrOwner(repositoryId, userId)
Repository repo = repositoryRepository.findByIdAndDeletedAtIsNull(repositoryId)
.orElseThrow(() -> new GlobalException(ErrorCode.REPOSITORY_NOT_FOUND));

boolean hasAccess = repositoryRepository.findByIdAndMemberOrOwner(repositoryId, userId).isPresent();
if (!hasAccess) {
throw new GlobalException(ErrorCode.REPOSITORY_ACCESS_DENIED);
}

// 2. 히스토리 목록(최신순) 조회
List<History> histories = historyRepository.findByRepositoryOrderByCreatedAtDesc(repo);

Expand All @@ -157,9 +171,14 @@ public List<HistoryListResponse> getHistories(Long repositoryId, Long userId) {
@Transactional
public HistoryRestoreResponse restoreHistory(Long repositoryId, Long historyId, Long userId) {
// 1. 레포/오너 확인
Repository repo = repositoryRepository.findById(repositoryId)
Repository repo = repositoryRepository.findByIdAndDeletedAtIsNull(repositoryId)
.orElseThrow(() -> new GlobalException(ErrorCode.REPOSITORY_NOT_FOUND));

boolean hasAccess = repositoryRepository.findByIdAndMemberOrOwner(repositoryId, userId).isPresent();
if (!hasAccess) {
throw new GlobalException(ErrorCode.REPOSITORY_ACCESS_DENIED);
}

if (!repo.getOwner().getId().equals(userId)) {
throw new GlobalException(ErrorCode.FORBIDDEN);
}
Expand Down