Skip to content

Commit 128aa70

Browse files
committed
feat: 실시간 협업 다중 세션 기능 구현(#34)
1 parent a703a80 commit 128aa70

25 files changed

+627
-195
lines changed
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package com.dmu.debug_visual.collab.domain.entity;
2+
3+
import jakarta.persistence.*;
4+
import lombok.AccessLevel;
5+
import lombok.Builder;
6+
import lombok.Getter;
7+
import lombok.NoArgsConstructor;
8+
import java.util.UUID;
9+
10+
@Entity
11+
@Getter
12+
@NoArgsConstructor(access = AccessLevel.PROTECTED)
13+
public class CodeSession {
14+
15+
@Id
16+
@GeneratedValue(strategy = GenerationType.IDENTITY)
17+
private Long id;
18+
19+
@Column(nullable = false, unique = true)
20+
private String sessionId; // 웹소켓 통신 등에 사용할 고유 ID
21+
22+
@Column(nullable = false)
23+
private String sessionName; // 사용자에게 보여질 세션 이름 (예: "main.java", "알고리즘 문제풀이")
24+
25+
@ManyToOne(fetch = FetchType.LAZY)
26+
@JoinColumn(name = "room_id", nullable = false)
27+
private Room room; // 이 세션이 속한 방
28+
29+
@Builder
30+
public CodeSession(String sessionName, Room room) {
31+
this.sessionId = UUID.randomUUID().toString();
32+
this.sessionName = sessionName;
33+
this.room = room;
34+
}
35+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package com.dmu.debug_visual.collab.domain.entity;
2+
3+
import com.dmu.debug_visual.user.User;
4+
import jakarta.persistence.*;
5+
import lombok.AccessLevel;
6+
import lombok.Builder;
7+
import lombok.Getter;
8+
import lombok.NoArgsConstructor;
9+
10+
import java.util.ArrayList;
11+
import java.util.List;
12+
import java.util.UUID;
13+
14+
@Entity
15+
@Getter
16+
@NoArgsConstructor(access = AccessLevel.PROTECTED)
17+
@Table(name = "collab_room") // DB 테이블 이름을 명확히 지정
18+
public class Room {
19+
20+
@Id
21+
@GeneratedValue(strategy = GenerationType.IDENTITY)
22+
private Long id;
23+
24+
@Column(nullable = false, unique = true)
25+
private String roomId; // 기존 DTO의 roomId와 동일한 역할 (외부용 ID)
26+
27+
@Column(nullable = false)
28+
private String name; // 방 이름 (예: "알고리즘 스터디")
29+
30+
@ManyToOne(fetch = FetchType.LAZY)
31+
@JoinColumn(name = "owner_id", nullable = false)
32+
private User owner; // 방을 생성한 방장
33+
34+
// 1:N 관계 - 이 방에 속한 코드 세션 목록
35+
@OneToMany(mappedBy = "room", cascade = CascadeType.ALL, orphanRemoval = true)
36+
private List<CodeSession> codeSessions = new ArrayList<>();
37+
38+
// 1:N 관계 - 이 방에 참여한 참여자 목록
39+
@OneToMany(mappedBy = "room", cascade = CascadeType.ALL, orphanRemoval = true)
40+
private List<RoomParticipant> participants = new ArrayList<>();
41+
42+
@Builder
43+
public Room(String name, User owner) {
44+
this.roomId = UUID.randomUUID().toString();
45+
this.name = name;
46+
this.owner = owner;
47+
}
48+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package com.dmu.debug_visual.collab.domain.entity;
2+
3+
import com.dmu.debug_visual.user.User;
4+
import jakarta.persistence.*;
5+
import lombok.AccessLevel;
6+
import lombok.Builder;
7+
import lombok.Getter;
8+
import lombok.NoArgsConstructor;
9+
10+
@Entity
11+
@Getter
12+
@NoArgsConstructor(access = AccessLevel.PROTECTED)
13+
public class RoomParticipant {
14+
15+
@Id
16+
@GeneratedValue(strategy = GenerationType.IDENTITY)
17+
private Long id;
18+
19+
@ManyToOne(fetch = FetchType.LAZY)
20+
@JoinColumn(name = "room_id", nullable = false)
21+
private Room room;
22+
23+
@ManyToOne(fetch = FetchType.LAZY)
24+
@JoinColumn(name = "user_id", nullable = false)
25+
private User user;
26+
27+
@Enumerated(EnumType.STRING)
28+
@Column(nullable = false)
29+
private Permission permission;
30+
31+
public enum Permission {
32+
READ_ONLY,
33+
READ_WRITE
34+
}
35+
36+
@Builder
37+
public RoomParticipant(Room room, User user, Permission permission) {
38+
this.room = room;
39+
this.user = user;
40+
this.permission = permission;
41+
}
42+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.dmu.debug_visual.collab.domain.repository;
2+
3+
import com.dmu.debug_visual.collab.domain.entity.CodeSession;
4+
import org.springframework.data.jpa.repository.JpaRepository;
5+
6+
public interface CodeSessionRepository extends JpaRepository<CodeSession, Long> {
7+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.dmu.debug_visual.collab.domain.repository;
2+
3+
import com.dmu.debug_visual.collab.domain.entity.Room;
4+
import com.dmu.debug_visual.collab.domain.entity.RoomParticipant;
5+
import org.springframework.data.jpa.repository.JpaRepository;
6+
7+
import java.util.Optional;
8+
9+
public interface RoomParticipantRepository extends JpaRepository<RoomParticipant, Long> {
10+
Optional<RoomParticipant> findByRoomAndUser_UserId(Room room, String userId);
11+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.dmu.debug_visual.collab.domain.repository;
2+
3+
import com.dmu.debug_visual.collab.domain.entity.Room;
4+
import org.springframework.data.jpa.repository.JpaRepository;
5+
6+
import java.util.Optional;
7+
8+
public interface RoomRepository extends JpaRepository<Room, Long> {
9+
// roomId (String)를 이용해 Room 엔티티를 조회하는 메소드
10+
Optional<Room> findByRoomId(String roomId);
11+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package com.dmu.debug_visual.collab.rest;
2+
3+
import com.dmu.debug_visual.security.CustomUserDetails;
4+
import com.dmu.debug_visual.collab.rest.dto.CreateRoomRequest;
5+
import com.dmu.debug_visual.collab.rest.dto.CreateSessionRequest;
6+
import com.dmu.debug_visual.collab.rest.dto.RoomResponse;
7+
import com.dmu.debug_visual.collab.rest.dto.SessionResponse;
8+
import com.dmu.debug_visual.collab.service.RoomService;
9+
import io.swagger.v3.oas.annotations.Operation;
10+
import io.swagger.v3.oas.annotations.Parameter;
11+
import io.swagger.v3.oas.annotations.media.Content;
12+
import io.swagger.v3.oas.annotations.media.Schema;
13+
import io.swagger.v3.oas.annotations.responses.ApiResponse;
14+
import io.swagger.v3.oas.annotations.responses.ApiResponses;
15+
import io.swagger.v3.oas.annotations.tags.Tag;
16+
import lombok.RequiredArgsConstructor;
17+
import org.springframework.http.ResponseEntity;
18+
import org.springframework.security.core.annotation.AuthenticationPrincipal;
19+
import org.springframework.web.bind.annotation.*;
20+
21+
@Tag(name = "협업 방 및 세션 관리 API", description = "실시간 협업을 위한 방과 코드 세션을 생성하고 관리합니다.")
22+
@RestController
23+
@RequestMapping("/api/collab-rooms")
24+
@RequiredArgsConstructor
25+
public class RoomController {
26+
27+
private final RoomService roomService; // DB 관련 서비스
28+
29+
@Operation(summary = "새로운 협업 방 생성", description = "DB에 새로운 협업 방을 생성하고, 방장(owner)을 첫 참여자로 자동 등록합니다.")
30+
@ApiResponses({
31+
@ApiResponse(responseCode = "200", description = "방 생성 성공",
32+
content = @Content(mediaType = "application/json", schema = @Schema(implementation = RoomResponse.class))),
33+
@ApiResponse(responseCode = "401", description = "인증 실패", content = @Content)
34+
})
35+
@PostMapping
36+
public ResponseEntity<RoomResponse> createRoom(
37+
@RequestBody CreateRoomRequest request,
38+
@AuthenticationPrincipal CustomUserDetails userDetails
39+
) {
40+
String ownerUserId = userDetails.getUsername();
41+
RoomResponse response = roomService.createRoom(request, ownerUserId);
42+
return ResponseEntity.ok(response);
43+
}
44+
45+
@Operation(summary = "방 안에 새 코드 세션 생성", description = "기존에 생성된 협업 방 안에 독립적인 새 코드 편집 세션을 생성합니다.")
46+
@ApiResponses({
47+
@ApiResponse(responseCode = "200", description = "세션 생성 성공",
48+
content = @Content(mediaType = "application/json", schema = @Schema(implementation = SessionResponse.class))),
49+
@ApiResponse(responseCode = "401", description = "인증 실패", content = @Content),
50+
@ApiResponse(responseCode = "403", description = "해당 방의 참여자가 아님", content = @Content),
51+
@ApiResponse(responseCode = "404", description = "존재하지 않는 방", content = @Content)
52+
})
53+
@PostMapping("/{roomId}/sessions")
54+
public ResponseEntity<SessionResponse> createSession(
55+
@Parameter(description = "세션을 생성할 방의 고유 ID (UUID)", required = true)
56+
@PathVariable String roomId,
57+
58+
@RequestBody CreateSessionRequest request,
59+
@AuthenticationPrincipal CustomUserDetails userDetails
60+
) {
61+
String creatorUserId = userDetails.getUsername();
62+
SessionResponse response = roomService.createCodeSessionInRoom(roomId, request, creatorUserId);
63+
return ResponseEntity.ok(response);
64+
}
65+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.dmu.debug_visual.collab.rest.dto;
2+
3+
import lombok.Getter;
4+
import lombok.NoArgsConstructor;
5+
6+
// 방 생성 요청 시 받을 데이터
7+
@Getter
8+
@NoArgsConstructor
9+
public class CreateRoomRequest {
10+
private String roomName;
11+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.dmu.debug_visual.collab.rest.dto;
2+
3+
import lombok.Getter;
4+
import lombok.NoArgsConstructor;
5+
6+
@Getter
7+
@NoArgsConstructor
8+
public class CreateSessionRequest {
9+
// 사용자가 입력할 세션의 이름 (예: "main.java", "Test Case 1")
10+
private String sessionName;
11+
}

src/main/java/com/dmu/debug_visual/websocket/dto/PermissionChangeMessage.java renamed to src/main/java/com/dmu/debug_visual/collab/rest/dto/PermissionChangeMessage.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package com.dmu.debug_visual.websocket.dto;
1+
package com.dmu.debug_visual.collab.rest.dto;
22

33
import lombok.Getter;
44
import lombok.Setter;

0 commit comments

Comments
 (0)