11package com .dmu .debug_visual .collab .service ;
22
33import 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 ;
46import com .dmu .debug_visual .collab .domain .repository .CodeSessionRepository ;
7+ import com .dmu .debug_visual .collab .domain .repository .SessionParticipantRepository ;
58import com .dmu .debug_visual .user .User ;
69import com .dmu .debug_visual .user .UserRepository ;
710import com .dmu .debug_visual .collab .domain .repository .RoomParticipantRepository ;
1720import org .springframework .stereotype .Service ;
1821import org .springframework .transaction .annotation .Transactional ;
1922
23+ /**
24+ * 협업 방과 세션의 생성, 관리, 권한 부여 등 핵심 비즈니스 로직을 처리하는 서비스
25+ */
2026@ Service
2127@ RequiredArgsConstructor
2228public 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