Skip to content

Commit d2d509f

Browse files
committed
feat: 세션 단위 권한 관리 및 생명주기 제어 기능 추가(#36)
1 parent 128aa70 commit d2d509f

File tree

6 files changed

+259
-80
lines changed

6 files changed

+259
-80
lines changed

src/main/java/com/dmu/debug_visual/collab/domain/entity/CodeSession.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55
import lombok.Builder;
66
import lombok.Getter;
77
import lombok.NoArgsConstructor;
8+
import org.hibernate.annotations.ColumnDefault;
9+
10+
import java.util.ArrayList;
11+
import java.util.List;
812
import java.util.UUID;
913

1014
@Entity
@@ -26,10 +30,28 @@ public class CodeSession {
2630
@JoinColumn(name = "room_id", nullable = false)
2731
private Room room; // 이 세션이 속한 방
2832

33+
@Enumerated(EnumType.STRING)
34+
@Column(nullable = false)
35+
@ColumnDefault("'ACTIVE'") // DB에 기본값을 'ACTIVE'로 설정
36+
private SessionStatus status;
37+
38+
public enum SessionStatus {
39+
ACTIVE, // 활성화 (방송 중)
40+
INACTIVE // 비활성화 (방송 꺼짐)
41+
}
42+
2943
@Builder
3044
public CodeSession(String sessionName, Room room) {
3145
this.sessionId = UUID.randomUUID().toString();
3246
this.sessionName = sessionName;
3347
this.room = room;
48+
this.status = SessionStatus.ACTIVE;
49+
}
50+
51+
public void updateStatus(SessionStatus status) {
52+
this.status = status;
3453
}
54+
55+
@OneToMany(mappedBy = "codeSession", cascade = CascadeType.ALL, orphanRemoval = true)
56+
private List<SessionParticipant> participants = new ArrayList<>();
3557
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
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 SessionParticipant {
14+
15+
@Id
16+
@GeneratedValue(strategy = GenerationType.IDENTITY)
17+
private Long id;
18+
19+
@ManyToOne(fetch = FetchType.LAZY)
20+
@JoinColumn(name = "code_session_id", nullable = false)
21+
private CodeSession codeSession;
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+
public void updatePermission(Permission permission) {
37+
this.permission = permission;
38+
}
39+
40+
@Builder
41+
public SessionParticipant(CodeSession codeSession, User user, Permission permission) {
42+
this.codeSession = codeSession;
43+
this.user = user;
44+
this.permission = permission;
45+
}
46+
}

src/main/java/com/dmu/debug_visual/collab/domain/repository/CodeSessionRepository.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,8 @@
33
import com.dmu.debug_visual.collab.domain.entity.CodeSession;
44
import org.springframework.data.jpa.repository.JpaRepository;
55

6+
import java.util.Optional;
7+
68
public interface CodeSessionRepository extends JpaRepository<CodeSession, Long> {
9+
Optional<CodeSession> findBySessionId(String sessionId);
710
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package com.dmu.debug_visual.collab.domain.repository;
2+
3+
import com.dmu.debug_visual.collab.domain.entity.SessionParticipant;
4+
import org.springframework.data.jpa.repository.JpaRepository;
5+
import org.springframework.data.jpa.repository.Modifying;
6+
import org.springframework.data.jpa.repository.Query;
7+
import org.springframework.data.repository.query.Param;
8+
9+
import java.util.Optional;
10+
11+
public interface SessionParticipantRepository extends JpaRepository<SessionParticipant, Long> {
12+
// ✨ 세션 ID와 유저 ID로 참여 정보를 찾는 메소드
13+
Optional<SessionParticipant> findByCodeSession_SessionIdAndUser_UserId(String sessionId, String userId);
14+
15+
// ✨ 강퇴 기능을 위해 특정 방의 모든 세션에서 특정 유저를 삭제하는 메소드
16+
@Modifying
17+
@Query("DELETE FROM SessionParticipant sp WHERE sp.codeSession.room.roomId = :roomId AND sp.user.userId = :userId")
18+
void deleteAllByRoomIdAndUserId(@Param("roomId") String roomId, @Param("userId") String userId);
19+
}
Lines changed: 156 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
package com.dmu.debug_visual.collab.service;
22

33
import com.dmu.debug_visual.collab.domain.entity.CodeSession;
4+
import com.dmu.debug_visual.collab.domain.entity.CodeSession.SessionStatus;
5+
import com.dmu.debug_visual.collab.domain.entity.SessionParticipant;
46
import com.dmu.debug_visual.collab.domain.repository.CodeSessionRepository;
7+
import com.dmu.debug_visual.collab.domain.repository.SessionParticipantRepository;
58
import com.dmu.debug_visual.user.User;
69
import com.dmu.debug_visual.user.UserRepository;
710
import com.dmu.debug_visual.collab.domain.repository.RoomParticipantRepository;
@@ -17,73 +20,204 @@
1720
import org.springframework.stereotype.Service;
1821
import org.springframework.transaction.annotation.Transactional;
1922

23+
/**
24+
* 협업 방과 세션의 생성, 관리, 권한 부여 등 핵심 비즈니스 로직을 처리하는 서비스
25+
*/
2026
@Service
2127
@RequiredArgsConstructor
2228
public class RoomService {
2329

2430
private final RoomRepository roomRepository;
2531
private final UserRepository userRepository;
2632
private final RoomParticipantRepository roomParticipantRepository;
33+
private final SessionParticipantRepository sessionParticipantRepository;
2734
private final CodeSessionRepository codeSessionRepository;
2835

36+
// 1. 방 관리 (Room Management)
37+
/**
38+
* 새로운 협업 방을 생성하고, 생성자를 방장 및 첫 참여자로 등록합니다.
39+
* @param request 방 이름이 담긴 요청 DTO
40+
* @param ownerUserId 방을 생성하는 사용자의 ID
41+
* @return 생성된 방의 정보가 담긴 응답 DTO
42+
*/
2943
@Transactional
3044
public RoomResponse createRoom(CreateRoomRequest request, String ownerUserId) {
31-
// 1. 방을 생성할 유저(방장) 정보를 DB에서 조회
3245
User owner = userRepository.findByUserId(ownerUserId)
33-
.orElseThrow(() -> new EntityNotFoundException("User not found with id: " + ownerUserId));
46+
.orElseThrow(() -> new EntityNotFoundException("User not found: " + ownerUserId));
3447

35-
// 2. 새로운 Room 엔티티 생성 및 저장
3648
Room newRoom = Room.builder()
3749
.name(request.getRoomName())
3850
.owner(owner)
3951
.build();
4052
roomRepository.save(newRoom);
4153

42-
// 3. 방장(Owner)을 첫 참여자로 등록 (권한은 READ_WRITE)
4354
RoomParticipant ownerParticipant = RoomParticipant.builder()
4455
.room(newRoom)
4556
.user(owner)
4657
.permission(RoomParticipant.Permission.READ_WRITE)
4758
.build();
4859
roomParticipantRepository.save(ownerParticipant);
4960

50-
// 4. 기본 코드 세션("main") 생성
51-
CodeSession defaultSession = CodeSession.builder()
52-
.sessionName("main") // 기본 세션 이름
53-
.room(newRoom)
54-
.build();
55-
codeSessionRepository.save(defaultSession);
56-
57-
// 5. 응답 DTO를 만들어 반환
5861
return RoomResponse.builder()
5962
.roomId(newRoom.getRoomId())
6063
.roomName(newRoom.getName())
6164
.ownerId(owner.getUserId())
62-
.defaultSessionId(defaultSession.getSessionId())
6365
.build();
6466
}
6567

68+
/**
69+
* 방장이 특정 참가자를 방에서 강퇴시킵니다.
70+
* @param roomId 대상 방의 ID
71+
* @param ownerId 요청을 보낸 방장의 ID
72+
* @param targetUserId 강퇴될 참가자의 ID
73+
*/
6674
@Transactional
67-
public SessionResponse createCodeSessionInRoom(String roomId, CreateSessionRequest request, String creatorUserId) {
68-
// 1. roomId로 해당 방을 DB에서 조회
75+
public void kickParticipant(String roomId, String ownerId, String targetUserId) {
6976
Room room = roomRepository.findByRoomId(roomId)
70-
.orElseThrow(() -> new EntityNotFoundException("Room not found with id: " + roomId));
77+
.orElseThrow(() -> new EntityNotFoundException("Room not found: " + roomId));
78+
79+
if (!room.getOwner().getUserId().equals(ownerId)) {
80+
throw new IllegalStateException("Only the room owner can kick participants.");
81+
}
82+
if (ownerId.equals(targetUserId)) {
83+
throw new IllegalArgumentException("Owner cannot kick themselves.");
84+
}
85+
86+
// 1. 방 참여자 목록에서 삭제
87+
RoomParticipant participantToRemove = roomParticipantRepository.findByRoomAndUser_UserId(room, targetUserId)
88+
.orElseThrow(() -> new EntityNotFoundException("Participant not found in this room."));
89+
roomParticipantRepository.delete(participantToRemove);
90+
91+
// 2. 해당 방의 모든 세션 참여자 목록에서도 삭제
92+
sessionParticipantRepository.deleteAllByRoomIdAndUserId(roomId, targetUserId);
93+
}
94+
95+
// 2. 세션 관리 (Session Management)
7196

72-
// 2. (보안) 요청자가 해당 방의 참여자인지 확인
73-
roomParticipantRepository.findByRoomAndUser_UserId(room, creatorUserId)
74-
.orElseThrow(() -> new IllegalStateException("You are not a participant of this room."));
97+
/**
98+
* 특정 방 안에 새로운 코드 세션을 생성합니다. (방송 시작)
99+
* 세션 생성자는 READ_WRITE, 나머지 방 멤버는 READ_ONLY 권한을 자동으로 부여받습니다.
100+
* @param roomId 세션을 생성할 방의 ID
101+
* @param request 세션 이름이 담긴 요청 DTO
102+
* @param creatorUserId 세션을 생성하는 사용자의 ID
103+
* @return 생성된 세션의 정보가 담긴 응답 DTO
104+
*/
105+
@Transactional
106+
public SessionResponse createCodeSessionInRoom(String roomId, CreateSessionRequest request, String creatorUserId) {
107+
Room room = roomRepository.findByRoomId(roomId)
108+
.orElseThrow(() -> new EntityNotFoundException("Room not found: " + roomId));
109+
User creator = userRepository.findByUserId(creatorUserId)
110+
.orElseThrow(() -> new EntityNotFoundException("User not found: " + creatorUserId));
75111

76-
// 3. 새로운 CodeSession 엔티티 생성
77112
CodeSession newSession = CodeSession.builder()
78113
.sessionName(request.getSessionName())
79114
.room(room)
80115
.build();
81116
codeSessionRepository.save(newSession);
82117

83-
// 4. 응답 DTO를 만들어 반환
118+
// 세션 생성자에게는 쓰기 권한 부여
119+
SessionParticipant creatorParticipant = SessionParticipant.builder()
120+
.codeSession(newSession)
121+
.user(creator)
122+
.permission(SessionParticipant.Permission.READ_WRITE)
123+
.build();
124+
sessionParticipantRepository.save(creatorParticipant);
125+
126+
// 방에 있는 다른 모든 참여자에게는 읽기 전용 권한 부여
127+
room.getParticipants().stream()
128+
.map(RoomParticipant::getUser)
129+
.filter(user -> !user.getUserId().equals(creatorUserId))
130+
.forEach(participantUser -> {
131+
SessionParticipant readOnlyParticipant = SessionParticipant.builder()
132+
.codeSession(newSession)
133+
.user(participantUser)
134+
.permission(SessionParticipant.Permission.READ_ONLY)
135+
.build();
136+
sessionParticipantRepository.save(readOnlyParticipant);
137+
});
138+
84139
return SessionResponse.builder()
85140
.sessionId(newSession.getSessionId())
86141
.sessionName(newSession.getSessionName())
87142
.build();
88143
}
89-
}
144+
145+
/**
146+
* 세션의 상태를 변경합니다. (방송 켜기/끄기)
147+
* @param sessionId 상태를 변경할 세션의 ID
148+
* @param userId 요청을 보낸 사용자의 ID
149+
* @param newStatus 변경할 새로운 상태 (ACTIVE / INACTIVE)
150+
*/
151+
@Transactional
152+
public void updateSessionStatus(String sessionId, String userId, SessionStatus newStatus) {
153+
CodeSession session = findSessionAndVerifyCreator(sessionId, userId, "Only the session creator can change the status.");
154+
session.updateStatus(newStatus);
155+
}
156+
157+
158+
// 3. 세션 권한 관리 (Permission Management)
159+
160+
/**
161+
* 특정 세션에 대한 사용자의 쓰기 권한 여부를 확인합니다.
162+
* @param sessionId 확인할 세션의 ID
163+
* @param userId 확인할 사용자의 ID
164+
* @return 쓰기 권한이 있으면 true, 아니면 false
165+
*/
166+
@Transactional(readOnly = true)
167+
public boolean hasWritePermissionInSession(String sessionId, String userId) {
168+
return sessionParticipantRepository.findByCodeSession_SessionIdAndUser_UserId(sessionId, userId)
169+
.map(participant -> participant.getPermission() == SessionParticipant.Permission.READ_WRITE)
170+
.orElse(false);
171+
}
172+
173+
/**
174+
* 세션 생성자가 다른 참여자에게 쓰기 권한을 부여합니다.
175+
* @param sessionId 권한을 부여할 세션의 ID
176+
* @param requesterId 권한을 부여하는 사용자(생성자)의 ID
177+
* @param targetUserId 권한을 받을 사용자의 ID
178+
*/
179+
@Transactional
180+
public void grantWritePermissionInSession(String sessionId, String requesterId, String targetUserId) {
181+
findSessionAndVerifyCreator(sessionId, requesterId, "Only the session creator can grant permissions.");
182+
183+
SessionParticipant participant = sessionParticipantRepository.findByCodeSession_SessionIdAndUser_UserId(sessionId, targetUserId)
184+
.orElseThrow(() -> new EntityNotFoundException("Participant not found in this session."));
185+
participant.updatePermission(SessionParticipant.Permission.READ_WRITE);
186+
}
187+
188+
/**
189+
* 세션 생성자가 다른 참여자의 쓰기 권한을 회수합니다.
190+
* @param sessionId 권한을 회수할 세션의 ID
191+
* @param requesterId 권한을 회수하는 사용자(생성자)의 ID
192+
* @param targetUserId 권한을 회수당할 사용자의 ID
193+
*/
194+
@Transactional
195+
public void revokeWritePermissionInSession(String sessionId, String requesterId, String targetUserId) {
196+
CodeSession session = findSessionAndVerifyCreator(sessionId, requesterId, "Only the session creator can revoke permissions.");
197+
198+
if (requesterId.equals(targetUserId)) {
199+
throw new IllegalArgumentException("Session creator cannot revoke their own permission.");
200+
}
201+
202+
SessionParticipant participant = sessionParticipantRepository.findByCodeSession_SessionIdAndUser_UserId(sessionId, targetUserId)
203+
.orElseThrow(() -> new EntityNotFoundException("Participant not found in this session."));
204+
participant.updatePermission(SessionParticipant.Permission.READ_ONLY);
205+
}
206+
207+
// Private Helper Methods
208+
209+
/**
210+
* 세션을 찾고, 요청자가 해당 세션의 생성자인지 검증하는 private 헬퍼 메소드
211+
*/
212+
private CodeSession findSessionAndVerifyCreator(String sessionId, String requesterId, String errorMessage) {
213+
CodeSession session = codeSessionRepository.findBySessionId(sessionId)
214+
.orElseThrow(() -> new EntityNotFoundException("Session not found: " + sessionId));
215+
216+
// 세션의 첫 번째 참여자가 생성자라는 규칙을 활용
217+
String creatorId = session.getParticipants().get(0).getUser().getUserId();
218+
if (!creatorId.equals(requesterId)) {
219+
throw new IllegalStateException(errorMessage);
220+
}
221+
return session;
222+
}
223+
}

0 commit comments

Comments
 (0)