Skip to content
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ dependencies {
// Development Tools
compileOnly("org.projectlombok:lombok")
annotationProcessor("org.projectlombok:lombok")
annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")
developmentOnly("org.springframework.boot:spring-boot-devtools")

// Swagger
Expand Down
4 changes: 3 additions & 1 deletion src/main/java/com/back/Application.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@SpringBootApplication
@EnableJpaAuditing
@SpringBootApplication
@ConfigurationPropertiesScan
public class Application {

public static void main(String[] args) {
Expand Down
5 changes: 0 additions & 5 deletions src/main/java/com/back/domain/studyroom/entity/Room.java
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,6 @@ public String getRawThumbnailUrl() {
private boolean allowAudio;
private boolean allowScreenShare;

@Builder.Default
@Column(nullable = false)
private Integer currentParticipants = 0;

// 방 상태
@Builder.Default
@Enumerated(EnumType.STRING)
Expand Down Expand Up @@ -191,7 +187,6 @@ public static Room create(String title, String description, boolean isPrivate,
room.allowAudio = useWebRTC; // WebRTC 사용 여부에 따라 설정
room.allowScreenShare = useWebRTC; // WebRTC 사용 여부에 따라 설정
room.status = RoomStatus.WAITING; // 생성 시 대기 상태
room.currentParticipants = 0;
room.createdBy = creator;
room.theme = theme;

Expand Down
3 changes: 2 additions & 1 deletion src/main/java/com/back/global/exception/ErrorCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ public enum ErrorCode {
// ======================== 학습 기록 관련 ========================
DURATION_MISMATCH(HttpStatus.BAD_REQUEST, "RECORD_001", "받은 duration과 계산된 duration이 5초 이상 차이납니다."),

// ======================== 알림 관련 ========================
// ======================== 메모 관련 ========================
MEMO_NOT_FOUND(HttpStatus.NOT_FOUND, "MEMO_001", "존재하지 않는 메모입니다."),

// ======================== 알림 관련 ========================
Expand Down Expand Up @@ -99,6 +99,7 @@ public enum ErrorCode {
WS_INVALID_REQUEST(HttpStatus.BAD_REQUEST, "WS_014", "잘못된 WebSocket 요청입니다."),
WS_INTERNAL_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "WS_015", "WebSocket 내부 오류가 발생했습니다."),
WS_CHAT_DELETE_FORBIDDEN(HttpStatus.FORBIDDEN, "WS_016", "채팅 삭제 권한이 없습니다. 방장 또는 부방장만 가능합니다."),
WS_TARGET_OFFLINE(HttpStatus.NOT_FOUND, "WS_017", "상대방이 오프라인 상태이거나 연결할 수 없습니다."),

// ======================== 커뮤니티 관련 ========================
POST_NOT_FOUND(HttpStatus.NOT_FOUND, "POST_001", "존재하지 않는 게시글입니다."),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,19 @@

public record WebSocketSessionInfo(
Long userId,
String username,
String sessionId,
LocalDateTime connectedAt,
LocalDateTime lastActiveAt,
Long currentRoomId
) {

// 새로운 세션 생성
public static WebSocketSessionInfo createNewSession(Long userId, String sessionId) {
public static WebSocketSessionInfo createNewSession(Long userId, String username, String sessionId) {
LocalDateTime now = LocalDateTime.now();
return new WebSocketSessionInfo(
userId,
username,
sessionId,
now, // connectedAt
now, // lastActiveAt
Expand All @@ -26,6 +28,7 @@ public static WebSocketSessionInfo createNewSession(Long userId, String sessionI
public WebSocketSessionInfo withUpdatedActivity() {
return new WebSocketSessionInfo(
userId,
username,
sessionId,
connectedAt,
LocalDateTime.now(),
Expand All @@ -37,6 +40,7 @@ public WebSocketSessionInfo withUpdatedActivity() {
public WebSocketSessionInfo withRoomId(Long newRoomId) {
return new WebSocketSessionInfo(
userId,
username,
sessionId,
connectedAt,
LocalDateTime.now(),
Expand All @@ -48,6 +52,7 @@ public WebSocketSessionInfo withRoomId(Long newRoomId) {
public WebSocketSessionInfo withoutRoom() {
return new WebSocketSessionInfo(
userId,
username,
sessionId,
connectedAt,
LocalDateTime.now(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,10 @@ public void handleWebSocketConnectListener(SessionConnectEvent event) {

String sessionId = headerAccessor.getSessionId();
Long userId = userDetails.getUserId();
String username = userDetails.getUsername();

// 세션 매니저에 등록 (TTL 10분 자동 설정)
sessionManager.addSession(userId, sessionId);
sessionManager.addSession(userId, username, sessionId);

log.info("WebSocket 연결 완료 - 사용자: {} ({}), 세션: {}",
userDetails.getUsername(), userId, sessionId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,14 @@ public class UserSessionService {
private final RedisSessionStore redisSessionStore;

// 세션 등록
public void registerSession(Long userId, String sessionId) {
public void registerSession(Long userId, String username, String sessionId) {
WebSocketSessionInfo existingSession = redisSessionStore.getUserSession(userId);
if (existingSession != null) {
terminateSession(existingSession.sessionId());
log.info("기존 세션 제거 후 새 세션 등록 - 사용자: {}", userId);
}

WebSocketSessionInfo newSession = WebSocketSessionInfo.createNewSession(userId, sessionId);
WebSocketSessionInfo newSession = WebSocketSessionInfo.createNewSession(userId, username, sessionId);
redisSessionStore.saveUserSession(userId, newSession);
redisSessionStore.saveSessionUserMapping(sessionId, userId);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ public class WebSocketSessionManager {
private final RoomParticipantService roomParticipantService;

// 사용자 세션 추가 (WebSocket 연결 시 호출)
public void addSession(Long userId, String sessionId) {
userSessionService.registerSession(userId, sessionId);
public void addSession(Long userId, String username, String sessionId) {
userSessionService.registerSession(userId, username, sessionId);
}

// 세션 제거 (WebSocket 연결 종료 시 호출)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.back.global.websocket.webrtc.config;

import org.springframework.boot.context.properties.ConfigurationProperties;
import java.util.List;

@ConfigurationProperties(prefix = "webrtc")
public record WebRTCProperties(
List<IceServer> iceServers
) {
// yml의 각 항목과 매핑될 내부 record
public record IceServer(String urls, String username, String credential) {}
}
Original file line number Diff line number Diff line change
@@ -1,58 +1,55 @@
package com.back.global.websocket.webrtc.controller;

import com.back.global.common.dto.RsData;
import com.back.global.websocket.webrtc.config.WebRTCProperties;
import com.back.global.websocket.webrtc.dto.ice.IceServer;
import com.back.global.websocket.webrtc.dto.ice.IceServerConfig;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/webrtc")
@EnableConfigurationProperties(WebRTCProperties.class)
@Tag(name = "WebRTC API", description = "WebRTC 시그널링 및 ICE 서버 관련 REST API")
public class WebRTCApiController {

// ICE 서버 설정 조회
private final WebRTCProperties webRTCProperties;

@GetMapping("/ice-servers")
@Operation(
summary = "ICE 서버 설정 조회",
description = "WebRTC 연결에 필요한 STUN/TURN 서버 정보를 조회합니다."
)
@ApiResponses({
@ApiResponse(responseCode = "200", description = "조회 성공"),
@ApiResponse(responseCode = "500", description = "서버 오류")
})
@Operation(summary = "ICE 서버 설정 조회")
public ResponseEntity<RsData<IceServerConfig>> getIceServers(
@Parameter(description = "사용자 ID (선택)") @RequestParam(required = false) Long userId,
@Parameter(description = "방 ID (선택)") @RequestParam(required = false) Long roomId) {

log.info("ICE 서버 설정 요청 - userId: {}, roomId: {}", userId, roomId);

// 기본 Google STUN 서버 사용
IceServerConfig config = IceServerConfig.withDefaultStunServers();
List<IceServer> iceServers =
webRTCProperties.iceServers().stream()
.map(s -> new IceServer(s.urls(), s.username(), s.credential()))
.toList();

IceServerConfig config = new IceServerConfig(iceServers);

log.info("ICE 서버 설정 제공 완료 - STUN 서버 {}개", config.iceServers().size());
log.info("ICE 서버 설정 제공 완료 - STUN/TURN 서버 {}개", config.iceServers().size());

return ResponseEntity
.status(HttpStatus.OK)
.body(RsData.success("ICE 서버 설정 조회 성공", config));
}

// WebRTC 서비스 상태 확인
@GetMapping("/health")
@Operation(
summary = "WebRTC 서비스 상태 확인",
description = "WebRTC 시그널링 서버의 상태를 확인합니다."
)
@ApiResponse(responseCode = "200", description = "정상 작동 중")
@Operation(summary = "WebRTC 서비스 상태 확인")
public ResponseEntity<RsData<String>> healthCheck() {
return ResponseEntity
.status(HttpStatus.OK)
Expand Down
Loading