Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
e508157
refactor: 스더티룸 권한에 대한 로직 개선
loseminho Oct 2, 2025
8b766f3
Merge remote-tracking branch 'origin/dev' into refactor/146
loseminho Oct 2, 2025
ef46ed0
fix: ci에서 통과 못한 테스트코드 수정
loseminho Oct 3, 2025
23e55ea
fix:rest api와 웹소켓 중간 경로 통합
loseminho Oct 4, 2025
2979e6e
fix:rest api와 웹소켓 중간 경로 통합
loseminho Oct 4, 2025
2de8631
fix: 병합 충돌 제어
loseminho Oct 4, 2025
e576231
Merge remote-tracking branch 'origin/dev' into refactor/146
loseminho Oct 5, 2025
98c9e4c
fix: 에러 확인을 위한 통합테스트 추가, Room.create()메서드 수정
loseminho Oct 5, 2025
8cb4561
refactor, feat
loseminho Oct 5, 2025
be970fd
Merge remote-tracking branch 'origin/dev' into refactor/146
loseminho Oct 5, 2025
57c38ae
refactor: redis 로직 최적화 및 중복 검증 로직 제거
loseminho Oct 7, 2025
ad752e7
fix : 병합 충돌 제어
loseminho Oct 7, 2025
1af4d1e
fix: 에러 번호 수정
loseminho Oct 8, 2025
483a0fc
feat: 스터디룸 방 비밀번호 변경 및 삭제 기능 구현
loseminho Oct 9, 2025
89971f2
Merge remote-tracking branch 'origin/dev' into feat-215
loseminho Oct 9, 2025
77ab976
fix:app-dev 제거
loseminho Oct 9, 2025
7a17167
feat: 웹소켓 기반 소극적 하트비트
loseminho Oct 9, 2025
dd229f2
Merge remote-tracking branch 'origin/dev' into feat/217
loseminho Oct 10, 2025
cf32230
feat: 스터디룸 썸네일 기능 추가 및 webrtc 설정 변경에서 주석처리
loseminho Oct 10, 2025
be00c11
Merge branch 'feat/217' of https://github.com/prgrms-web-devcourse-fi…
loseminho Oct 10, 2025
da453f6
fix:소극적 하트비트 사용 주석처리
loseminho Oct 10, 2025
6ee41ed
Merge branch 'dev' into feat/217
loseminho Oct 10, 2025
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 @@ -64,7 +64,8 @@ public ResponseEntity<RsData<RoomResponse>> createRoom(
request.getPassword(),
request.getMaxParticipants() != null ? request.getMaxParticipants() : 10,
currentUserId,
request.getUseWebRTC() != null ? request.getUseWebRTC() : true // 디폴트: true
request.getUseWebRTC() != null ? request.getUseWebRTC() : true, // 디폴트: true
request.getThumbnailUrl() // 썸네일 URL
);

RoomResponse response = roomService.toRoomResponse(room);
Expand Down Expand Up @@ -339,7 +340,7 @@ public ResponseEntity<RsData<List<MyRoomResponse>>> getMyRooms() {
@PutMapping("/{roomId}")
@Operation(
summary = "방 설정 수정",
description = "방의 제목, 설명, 정원, RTC 설정 등을 수정합니다. 방장만 수정 가능합니다."
description = "방의 제목, 설명, 정원, 썸네일을 수정합니다. 방장만 수정 가능합니다. WebRTC 설정은 현재 수정 불가합니다."
)
@ApiResponses({
@ApiResponse(responseCode = "200", description = "수정 성공"),
Expand All @@ -359,9 +360,7 @@ public ResponseEntity<RsData<Void>> updateRoom(
request.getTitle(),
request.getDescription(),
request.getMaxParticipants(),
request.getAllowCamera() != null ? request.getAllowCamera() : true,
request.getAllowAudio() != null ? request.getAllowAudio() : true,
request.getAllowScreenShare() != null ? request.getAllowScreenShare() : true,
request.getThumbnailUrl(),
currentUserId
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ public class CreateRoomRequest {
@Size(max = 500, message = "방 설명은 500자를 초과할 수 없습니다")
private String description;

// 방 썸네일 이미지 URL (선택)
@Size(max = 500, message = "썸네일 URL은 500자를 초과할 수 없습니다")
private String thumbnailUrl;

private Boolean isPrivate = false;

private String password;
Expand Down
3 changes: 3 additions & 0 deletions src/main/java/com/back/domain/studyroom/dto/RoomResponse.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public class RoomResponse {
private String title;
private String description;
private Boolean isPrivate; // 비공개 방 여부 (UI에서 🔒 아이콘 표시용)
private String thumbnailUrl; // 썸네일 이미지 URL
private int currentParticipants;
private int maxParticipants;
private RoomStatus status;
Expand All @@ -31,6 +32,7 @@ public static RoomResponse from(Room room, long currentParticipants) {
.title(room.getTitle())
.description(room.getDescription() != null ? room.getDescription() : "")
.isPrivate(room.isPrivate()) // 비공개 방 여부
.thumbnailUrl(room.getThumbnailUrl()) // 썸네일 URL
.currentParticipants((int) currentParticipants) // Redis에서 조회한 실시간 값
.maxParticipants(room.getMaxParticipants())
.status(room.getStatus())
Expand All @@ -52,6 +54,7 @@ public static RoomResponse fromMasked(Room room) {
.title("🔒 비공개 방") // 제목 마스킹
.description("비공개 방입니다") // 설명 마스킹
.isPrivate(true)
.thumbnailUrl(null) // 썸네일 숨김
.currentParticipants(0) // 참가자 수 숨김
.maxParticipants(0) // 정원 숨김
.status(room.getStatus())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,15 @@ public class UpdateRoomSettingsRequest {
@Max(value = 100, message = "최대 100명까지 가능합니다")
private Integer maxParticipants;

private Boolean allowCamera = true;
private Boolean allowAudio = true;
private Boolean allowScreenShare = true;
// 방 썸네일 이미지 URL (선택)
@Size(max = 500, message = "썸네일 URL은 500자를 초과할 수 없습니다")
private String thumbnailUrl;

// ===== WebRTC 설정 (추후 팀원 구현 시 주석 해제) =====
// WebRTC 기능은 방 생성 이후 별도 API로 관리 예정
// 현재는 방 생성 시의 useWebRTC 값으로만 초기 설정됨

// private Boolean allowCamera = true;
// private Boolean allowAudio = true;
// private Boolean allowScreenShare = true;
}
45 changes: 39 additions & 6 deletions src/main/java/com/back/domain/studyroom/entity/Room.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,31 @@ public class Room extends BaseEntity {
private String password;
private int maxParticipants;
private boolean isActive;

// 방 썸네일 이미지 URL
private String thumbnailUrl;

// 디폴트 썸네일 URL
private static final String DEFAULT_THUMBNAIL_URL = "/images/default-room-thumbnail.png";

/**
* 썸네일 URL 조회 (디폴트 처리 포함)
* null인 경우 디폴트 이미지 반환
*/
public String getThumbnailUrl() {
return (thumbnailUrl != null && !thumbnailUrl.trim().isEmpty())
? thumbnailUrl
: DEFAULT_THUMBNAIL_URL;
}

/**
* 원본 썸네일 URL 조회 (디폴트 처리 없음)
* DB에 실제 저장된 값 그대로 반환
*/
public String getRawThumbnailUrl() {
return thumbnailUrl;
}

private boolean allowCamera;
private boolean allowAudio;
private boolean allowScreenShare;
Expand Down Expand Up @@ -153,14 +178,15 @@ public boolean isOwner(Long userId) {
*/
public static Room create(String title, String description, boolean isPrivate,
String password, int maxParticipants, User creator, RoomTheme theme,
boolean useWebRTC) {
boolean useWebRTC, String thumbnailUrl) {
Room room = new Room();
room.title = title;
room.description = description;
room.isPrivate = isPrivate;
room.password = password;
room.maxParticipants = maxParticipants;
room.isActive = true; // 생성 시 기본적으로 활성화
room.thumbnailUrl = thumbnailUrl; // 썸네일 URL
room.allowCamera = useWebRTC; // WebRTC 사용 여부에 따라 설정
room.allowAudio = useWebRTC; // WebRTC 사용 여부에 따라 설정
room.allowScreenShare = useWebRTC; // WebRTC 사용 여부에 따라 설정
Expand All @@ -179,15 +205,22 @@ public static Room create(String title, String description, boolean isPrivate,
}

/**
* 방 설정 일괄 업데이트 메서드
방장이 방 설정을 변경할 때 여러 필드를 한 번에 업데이트
주된 생성 이유.. rtc 단체 제어를 위해 잡아놓았음. 잡아준 필드 변경 가능성 농후!!
* 방 설정 일괄 업데이트 메서드 (썸네일 포함)
* 방장이 방 설정을 변경할 때 여러 필드를 한 번에 업데이트
* WebRTC 설정은 제외 (확장성을 위해 제거가 아닌 주석으로 현재 놔둠..)
*/
public void updateSettings(String title, String description, int maxParticipants,
boolean allowCamera, boolean allowAudio, boolean allowScreenShare) {
public void updateSettings(String title, String description, int maxParticipants, String thumbnailUrl) {
this.title = title;
this.description = description;
this.maxParticipants = maxParticipants;
this.thumbnailUrl = thumbnailUrl;
}

/**
* WebRTC 설정 업데이트 메서드 (추후 사용 가능!)
* 현재는 미사용 - 추후 팀원이 만약 구현 시 활성화 되도록
*/
public void updateWebRTCSettings(boolean allowCamera, boolean allowAudio, boolean allowScreenShare) {
this.allowCamera = allowCamera;
this.allowAudio = allowAudio;
this.allowScreenShare = allowScreenShare;
Expand Down
17 changes: 8 additions & 9 deletions src/main/java/com/back/domain/studyroom/service/RoomService.java
Original file line number Diff line number Diff line change
Expand Up @@ -67,21 +67,21 @@ public class RoomService {
*/
@Transactional
public Room createRoom(String title, String description, boolean isPrivate,
String password, int maxParticipants, Long creatorId, boolean useWebRTC) {
String password, int maxParticipants, Long creatorId, boolean useWebRTC, String thumbnailUrl) {

User creator = userRepository.findById(creatorId)
.orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));

Room room = Room.create(title, description, isPrivate, password, maxParticipants, creator, null, useWebRTC);
Room room = Room.create(title, description, isPrivate, password, maxParticipants, creator, null, useWebRTC, thumbnailUrl);
Room savedRoom = roomRepository.save(room);

RoomMember hostMember = RoomMember.createHost(savedRoom, creator);
roomMemberRepository.save(hostMember);

// savedRoom.incrementParticipant(); // Redis로 이관 - DB 업데이트 제거

log.info("방 생성 완료 - RoomId: {}, Title: {}, CreatorId: {}, WebRTC: {}",
savedRoom.getId(), title, creatorId, useWebRTC);
log.info("방 생성 완료 - RoomId: {}, Title: {}, CreatorId: {}, WebRTC: {}, Thumbnail: {}",
savedRoom.getId(), title, creatorId, useWebRTC, thumbnailUrl != null ? "설정됨" : "없음");

return savedRoom;
}
Expand Down Expand Up @@ -268,8 +268,7 @@ public List<Room> getUserRooms(Long userId) {

@Transactional
public void updateRoomSettings(Long roomId, String title, String description,
int maxParticipants, boolean allowCamera,
boolean allowAudio, boolean allowScreenShare, Long userId) {
int maxParticipants, String thumbnailUrl, Long userId) {

Room room = roomRepository.findById(roomId)
.orElseThrow(() -> new CustomException(ErrorCode.ROOM_NOT_FOUND));
Expand All @@ -285,10 +284,10 @@ public void updateRoomSettings(Long roomId, String title, String description,
throw new CustomException(ErrorCode.BAD_REQUEST);
}

room.updateSettings(title, description, maxParticipants,
allowCamera, allowAudio, allowScreenShare);
room.updateSettings(title, description, maxParticipants, thumbnailUrl);

log.info("방 설정 변경 완료 - RoomId: {}, UserId: {}", roomId, userId);
log.info("방 설정 변경 완료 - RoomId: {}, UserId: {}, Thumbnail: {}",
roomId, userId, thumbnailUrl != null ? "변경됨" : "없음");
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.context.annotation.Bean;
import org.springframework.core.annotation.Order;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
Expand All @@ -16,6 +17,8 @@
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.messaging.support.ChannelInterceptor;
import org.springframework.messaging.support.MessageHeaderAccessor;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.security.core.Authentication;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
Expand All @@ -36,14 +39,37 @@ public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
* - /topic: 1:N 브로드캐스트 (방 채팅)
* - /queue: 1:1 메시지 (개인 DM)
* - /app: 클라이언트에서 서버로 메시지 전송 시 prefix
*
* STOMP 하트비트 설정(임시 주석 상태):
* - 25초마다 자동 하트비트 전송 (쓰기 비활성 시)
* - 25초 이상 응답 없으면 연결 종료 (읽기 비활성 시)
*/
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic", "/queue");
//.setHeartbeatValue(new long[]{25000, 25000}) // [서버→클라이언트, 클라이언트→서버]
//.setTaskScheduler(heartBeatScheduler());

config.setApplicationDestinationPrefixes("/app");
config.setUserDestinationPrefix("/user");
}

/**(임시 주석 상태)
* STOMP 하트비트 전용 스케줄러!!
* - 별도 스레드 풀로 하트비트 처리
* - 메인 비즈니스 로직에 영향 없음

@Bean
public TaskScheduler heartBeatScheduler() {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(1);
scheduler.setThreadNamePrefix("wss-heartbeat-");
scheduler.initialize();
log.info("STOMP 하트비트 스케줄러 초기화 완료 - 주기: 25초");
return scheduler;
}
*/

/**
* STOMP 엔드포인트 등록
* 클라이언트가 WebSocket 연결을 위해 사용할 엔드포인트
Expand Down Expand Up @@ -124,24 +150,22 @@ private void authenticateUser(StompHeaderAccessor accessor) {
}

/**
* 메시지 전송 시 인증 상태 확인 및 활동 시간 업데이트
* 메시지 전송 시 인증 상태 확인
*/
private void validateAuthenticationAndUpdateActivity(StompHeaderAccessor accessor) {
if (accessor.getUser() == null) {
throw new RuntimeException("인증이 필요합니다");
}

// 인증된 사용자 정보 추출 및 활동 시간 업데이트
Authentication auth = (Authentication) accessor.getUser();
if (auth.getPrincipal() instanceof CustomUserDetails userDetails) {
Long userId = userDetails.getUserId();

// 사용자 활동 시간 업데이트 (Heartbeat 효과)
// 전역 세션 활동 시간 업데이트
try {
sessionManager.updateLastActivity(userId);
} catch (Exception e) {
log.warn("활동 시간 업데이트 실패 - 사용자: {}, 오류: {}", userId, e.getMessage());
// 활동 시간 업데이트 실패해도 메시지 전송은 계속 진행
}

log.debug("인증된 사용자 메시지 전송 - 사용자: {} (ID: {}), 목적지: {}",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@ void setUp() {
10,
actor,
null,
true
true, // useWebRTC
null // thumbnailUrl
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,8 @@ private Room createRoom(User owner) {
20, // 최대 20명
owner,
null, // 테마 없음
true // WebRTC 활성화
true, // WebRTC 활성화
null // thumbnailUrl
);

testRoom = roomRepository.save(testRoom);
Expand Down
Loading