diff --git a/src/main/java/com/back/domain/file/entity/EntityType.java b/src/main/java/com/back/domain/file/entity/EntityType.java index 9f4bf77a..036ac93f 100644 --- a/src/main/java/com/back/domain/file/entity/EntityType.java +++ b/src/main/java/com/back/domain/file/entity/EntityType.java @@ -1,5 +1,5 @@ package com.back.domain.file.entity; public enum EntityType { - POST, AVATAR, PROFILE -} \ No newline at end of file + POST, AVATAR, PROFILE, ROOM_THUMBNAIL +} diff --git a/src/main/java/com/back/domain/file/service/FileService.java b/src/main/java/com/back/domain/file/service/FileService.java index dd55d6a5..dd89f5a4 100644 --- a/src/main/java/com/back/domain/file/service/FileService.java +++ b/src/main/java/com/back/domain/file/service/FileService.java @@ -164,4 +164,77 @@ private void checkAccessPermission(FileAttachment fileAttachment, Long userId) { throw new CustomException(ErrorCode.FILE_ACCESS_DENIED); } } + + /** + * URL이 우리 S3 버킷의 파일인지 확인 + * @param url 확인할 URL + * @return S3 파일이면 true, 외부 URL이면 false + */ + public boolean isOurS3File(String url) { + if (url == null || url.trim().isEmpty()) { + return false; + } + + // S3 URL 패턴 체크 + // 패턴 1: https://bucket-name.s3.amazonaws.com/... + // 패턴 2: https://s3.amazonaws.com/bucket-name/... + // 패턴 3: https://bucket-name.s3.ap-northeast-2.amazonaws.com/... + return url.contains(".s3.") && url.contains(".amazonaws.com") && url.contains(bucket); + } + + /** + * S3 URL에서 파일명(Key) 추출 + * @param url S3 전체 URL + * @return 파일명 (예: "uuid-filename.jpg") + */ + public String extractFileNameFromUrl(String url) { + if (url == null || url.trim().isEmpty()) { + return null; + } + + try { + // URL에서 마지막 "/" 이후의 파일명 추출 + int lastSlashIndex = url.lastIndexOf('/'); + if (lastSlashIndex >= 0 && lastSlashIndex < url.length() - 1) { + return url.substring(lastSlashIndex + 1); + } + } catch (Exception e) { + // 추출 실패 시 null 반환 + } + + return null; + } + + /** + * S3 파일을 파일명으로 삭제 + * RoomService 등 다른 도메인에서 썸네일 삭제 시 사용 + * @param fileName S3에 저장된 파일명 + */ + public void deleteS3FileByName(String fileName) { + if (fileName == null || fileName.trim().isEmpty()) { + return; + } + + try { + s3Delete(fileName); + } catch (Exception e) { + // S3 삭제 실패해도 무시 (파일이 이미 없을 수 있음) + // 로그만 남기고 계속 진행 + } + } + + /** + * URL로 S3 파일 삭제 (권한 체크 없음 - 내부 사용용) + * 우리 S3 파일인지 확인 후 삭제 + * @param url 삭제할 파일의 전체 URL + */ + public void deleteS3FileByUrl(String url) { + if (!isOurS3File(url)) { + // 외부 URL이면 삭제하지 않음 + return; + } + + String fileName = extractFileNameFromUrl(url); + deleteS3FileByName(fileName); + } } \ No newline at end of file diff --git a/src/main/java/com/back/domain/studyroom/dto/CreateRoomRequest.java b/src/main/java/com/back/domain/studyroom/dto/CreateRoomRequest.java index 8c1adaeb..85004bc3 100644 --- a/src/main/java/com/back/domain/studyroom/dto/CreateRoomRequest.java +++ b/src/main/java/com/back/domain/studyroom/dto/CreateRoomRequest.java @@ -16,7 +16,9 @@ public class CreateRoomRequest { @Size(max = 500, message = "방 설명은 500자를 초과할 수 없습니다") private String description; - // 방 썸네일 이미지 URL (선택) + // 방 썸네일 이미지 URL (선택해서 진행 가능) + // 파일 업로드를 사용하는 경우: 먼저 /api/files/upload로 파일을 업로드하고 받은 URL을 여기에 설정 + // 직접 URL을 입력하는 경우: 외부 이미지 URL을 직접 입력하게 됨 @Size(max = 500, message = "썸네일 URL은 500자를 초과할 수 없습니다") private String thumbnailUrl; diff --git a/src/main/java/com/back/domain/studyroom/dto/MyRoomResponse.java b/src/main/java/com/back/domain/studyroom/dto/MyRoomResponse.java index 548a6f09..f9d5fd47 100644 --- a/src/main/java/com/back/domain/studyroom/dto/MyRoomResponse.java +++ b/src/main/java/com/back/domain/studyroom/dto/MyRoomResponse.java @@ -15,6 +15,7 @@ public class MyRoomResponse { private String title; private String description; private Boolean isPrivate; // 비공개 방 여부 (UI에서 🔒 아이콘 표시용) + private String thumbnailUrl; // 썸네일 이미지 URL private int currentParticipants; private int maxParticipants; private RoomStatus status; @@ -26,8 +27,9 @@ public static MyRoomResponse of(Room room, long currentParticipants, RoomRole my .roomId(room.getId()) .title(room.getTitle()) .description(room.getDescription() != null ? room.getDescription() : "") - .isPrivate(room.isPrivate()) // 비공개 방 여부 - .currentParticipants((int) currentParticipants) // Redis에서 조회한 실시간 값 + .isPrivate(room.isPrivate()) + .thumbnailUrl(room.getThumbnailUrl()) + .currentParticipants((int) currentParticipants) .maxParticipants(room.getMaxParticipants()) .status(room.getStatus()) .myRole(myRole) diff --git a/src/main/java/com/back/domain/studyroom/dto/UpdateRoomSettingsRequest.java b/src/main/java/com/back/domain/studyroom/dto/UpdateRoomSettingsRequest.java index 974ce652..ebe41edd 100644 --- a/src/main/java/com/back/domain/studyroom/dto/UpdateRoomSettingsRequest.java +++ b/src/main/java/com/back/domain/studyroom/dto/UpdateRoomSettingsRequest.java @@ -21,6 +21,9 @@ public class UpdateRoomSettingsRequest { private Integer maxParticipants; // 방 썸네일 이미지 URL (선택) + // 파일 업로드를 사용하는 경우: 먼저 /api/files/upload로 파일을 업로드하고 받은 URL을 여기에 설정 + // 직접 URL을 입력하는 경우: 외부 이미지 URL을 직접 입력 + // null인 경우: 기존 썸네일 유지 @Size(max = 500, message = "썸네일 URL은 500자를 초과할 수 없습니다") private String thumbnailUrl; diff --git a/src/main/java/com/back/domain/studyroom/service/RoomService.java b/src/main/java/com/back/domain/studyroom/service/RoomService.java index 460b64e7..e717373d 100644 --- a/src/main/java/com/back/domain/studyroom/service/RoomService.java +++ b/src/main/java/com/back/domain/studyroom/service/RoomService.java @@ -52,6 +52,7 @@ public class RoomService { private final SimpMessagingTemplate messagingTemplate; private final ApplicationEventPublisher eventPublisher; private final AvatarService avatarService; + private final com.back.domain.file.service.FileService fileService; /** * 방 생성 메서드 @@ -78,8 +79,6 @@ public Room createRoom(String title, String description, boolean isPrivate, RoomMember hostMember = RoomMember.createHost(savedRoom, creator); roomMemberRepository.save(hostMember); - - // savedRoom.incrementParticipant(); // Redis로 이관 - DB 업데이트 제거 log.info("방 생성 완료 - RoomId: {}, Title: {}, CreatorId: {}, WebRTC: {}, Thumbnail: {}", savedRoom.getId(), title, creatorId, useWebRTC, thumbnailUrl != null ? "설정됨" : "없음"); @@ -196,7 +195,6 @@ public Page getJoinableRooms(Pageable pageable) { /** * 모든 방 조회 (공개 + 비공개 전체) - * 비공개 방은 정보 마스킹 */ public Page getAllRooms(Pageable pageable) { return roomRepository.findAllRooms(pageable); @@ -228,7 +226,7 @@ public Page getMyHostingRooms(Long userId, Pageable pageable) { /** * 모든 방을 RoomResponse로 변환 (비공개 방 마스킹 포함) * @param rooms 방 목록 - * @return 마스킹된 RoomResponse 리스트 + * @return RoomResponse 리스트 */ public java.util.List toRoomResponseListWithMasking(java.util.List rooms) { java.util.List roomIds = rooms.stream() @@ -257,8 +255,6 @@ public Room getRoomDetail(Long roomId, Long userId) { Room room = roomRepository.findById(roomId) .orElseThrow(() -> new CustomException(ErrorCode.ROOM_NOT_FOUND)); - // ⭐ 비공개 방 접근 제한 제거 - 모든 사용자가 조회 가능 - // (프론트엔드에서 입장 시 로그인 체크) return room; } @@ -285,8 +281,18 @@ public void updateRoomSettings(Long roomId, String title, String description, throw new CustomException(ErrorCode.BAD_REQUEST); } + // 기존 썸네일 URL 저장 + String oldThumbnailUrl = room.getRawThumbnailUrl(); + + // 방 설정 업데이트 room.updateSettings(title, description, maxParticipants, thumbnailUrl); + // 썸네일이 변경되었고, 기존 썸네일이 S3 파일인 경우 삭제 + if (oldThumbnailUrl != null && !oldThumbnailUrl.equals(thumbnailUrl)) { + fileService.deleteS3FileByUrl(oldThumbnailUrl); + log.info("기존 썸네일 삭제 완료 - RoomId: {}, OldUrl: {}", roomId, oldThumbnailUrl); + } + log.info("방 설정 변경 완료 - RoomId: {}, UserId: {}, Thumbnail: {}", roomId, userId, thumbnailUrl != null ? "변경됨" : "없음"); } @@ -353,6 +359,13 @@ public void terminateRoom(Long roomId, Long userId) { throw new CustomException(ErrorCode.NOT_ROOM_MANAGER); } + // 썸네일이 S3 파일인 경우 삭제 + String thumbnailUrl = room.getRawThumbnailUrl(); + if (thumbnailUrl != null) { + fileService.deleteS3FileByUrl(thumbnailUrl); + log.info("방 종료 - 썸네일 삭제 완료 - RoomId: {}, ThumbnailUrl: {}", roomId, thumbnailUrl); + } + room.terminate(); // Redis에서 모든 온라인 사용자 제거 @@ -530,7 +543,7 @@ public List getRoomMembers(Long roomId, Long userId) { Room room = roomRepository.findById(roomId) .orElseThrow(() -> new CustomException(ErrorCode.ROOM_NOT_FOUND)); - // ⭐ 비공개 방 접근 제한 제거 - 모든 사용자가 조회 가능 + // 모든 사용자가 조회 가능 // 1. Redis에서 온라인 사용자 ID 조회 Set onlineUserIds = roomParticipantService.getParticipants(roomId);