Skip to content

Commit 1159989

Browse files
authored
Feat: 스터디 룸 관련 기초 비즈니스 로직 테스트 체크 후 프로젝트와 통합 (#53)
1 parent 0b2b38e commit 1159989

File tree

11 files changed

+1334
-27
lines changed

11 files changed

+1334
-27
lines changed

src/main/java/com/back/domain/studyroom/controller/RoomController.java

Lines changed: 384 additions & 0 deletions
Large diffs are not rendered by default.

src/main/java/com/back/domain/studyroom/entity/Room.java

Lines changed: 144 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,40 +15,172 @@
1515
@Getter
1616
public class Room extends BaseEntity {
1717
private String title;
18-
1918
private String description;
20-
2119
private boolean isPrivate;
22-
2320
private String password;
24-
2521
private int maxParticipants;
26-
2722
private boolean isActive;
28-
2923
private boolean allowCamera;
30-
3124
private boolean allowAudio;
32-
3325
private boolean allowScreenShare;
3426

27+
// 방 상태
28+
@Enumerated(EnumType.STRING)
29+
@Column(nullable = false)
30+
private RoomStatus status = RoomStatus.WAITING;
31+
// 현재 참여자
32+
@Column(nullable = false)
33+
private int currentParticipants = 0;
34+
// 방장
3535
@ManyToOne(fetch = FetchType.LAZY)
3636
@JoinColumn(name = "created_by")
3737
private User createdBy;
38-
38+
// 테마
3939
@ManyToOne(fetch = FetchType.LAZY)
4040
@JoinColumn(name = "theme_id")
4141
private RoomTheme theme;
4242

43+
// 연관관계 설정
4344
@OneToMany(mappedBy = "room", cascade = CascadeType.ALL, orphanRemoval = true)
4445
private List<RoomMember> roomMembers = new ArrayList<>();
45-
46+
// 채팅 메시지
4647
@OneToMany(mappedBy = "room", cascade = CascadeType.ALL, orphanRemoval = true)
4748
private List<RoomChatMessage> roomChatMessages = new ArrayList<>();
48-
49+
// 참가자 기록
4950
@OneToMany(mappedBy = "room", cascade = CascadeType.ALL, orphanRemoval = true)
5051
private List<RoomParticipantHistory> roomParticipantHistories = new ArrayList<>();
51-
52+
// 스터디 기록
5253
@OneToMany(mappedBy = "room", cascade = CascadeType.ALL, orphanRemoval = true)
5354
public List<StudyRecord> studyRecords = new ArrayList<>();
55+
56+
57+
/**
58+
* 방에 입장할 수 있는지 확인
59+
* 사용 상황: 사용자가 방 입장을 시도할 때 입장 가능 여부를 미리 체크
60+
방이 활성화되어 있고, 입장 가능한 상태이며, 정원이 초과되지 않은 경우
61+
*/
62+
public boolean canJoin() {
63+
return isActive && status.isJoinable() && currentParticipants < maxParticipants;
64+
}
65+
66+
/**
67+
* 방의 정원이 가득 찼는지 확인
68+
방 목록에서 "만석" 표시하거나, 입장 제한할 때
69+
*/
70+
public boolean isFull() {
71+
return currentParticipants >= maxParticipants;
72+
}
73+
74+
/**
75+
* 참가자 수 증가 (최대 정원까지만)
76+
누군가 방에 입장했을 때 참가자 수를 1 증가시킴
77+
최대 정원을 초과하지 않도록 체크
78+
*/
79+
public void incrementParticipant() {
80+
if (currentParticipants < maxParticipants) {
81+
this.currentParticipants++;
82+
}
83+
}
84+
85+
/**
86+
* 참가자 수 감소 (0 이하로는 감소하지 않음)
87+
누군가 방에서 나갔을 때 참가자 수를 1 감소시킴
88+
음수가 되지 않도록 체크
89+
*/
90+
public void decrementParticipant() {
91+
if (this.currentParticipants > 0) {
92+
this.currentParticipants--;
93+
}
94+
}
95+
96+
/**
97+
* 비밀번호 입력이 필요한 방인지 확인
98+
비공개 방에 입장할 때 비밀번호 입력 폼을 보여줄지 결정
99+
비공개 방이면서 실제로 비밀번호가 설정되어 있는 경우
100+
*/
101+
public boolean needsPassword() {
102+
return isPrivate && password != null && !password.trim().isEmpty();
103+
}
104+
105+
/**
106+
* 방을 활성 상태로 변경
107+
방장이 스터디를 시작할 때 또는 대기 중인 방을 활성화할 때
108+
*/
109+
public void activate() {
110+
this.status = RoomStatus.ACTIVE;
111+
this.isActive = true;
112+
}
113+
114+
/**
115+
* 방을 일시 정지 상태로 변경
116+
*/
117+
public void pause() {
118+
this.status = RoomStatus.PAUSED;
119+
}
120+
121+
/**
122+
* 방을 종료 상태로 변경
123+
방장이 스터디를 완전히 끝내거나, 빈 방을 자동 정리할 때 (종료 처리를 어떻게 뺄지에 따라 변경 될 예정)
124+
*/
125+
public void terminate() {
126+
this.status = RoomStatus.TERMINATED;
127+
this.isActive = false;
128+
}
129+
130+
/**
131+
* 특정 사용자가 이 방의 소유자(방장)인지 확인
132+
방 설정 변경, 방 종료, 멤버 추방 등의 권한이 필요한 작업 전에 체크
133+
*/
134+
public boolean isOwner(Long userId) {
135+
return createdBy != null && createdBy.getId().equals(userId);
136+
}
137+
138+
/**
139+
* 방 생성을 위한 정적 팩토리 메서드
140+
새로운 방을 생성할 때 모든 기본값을 설정 해주는 초기 메서드
141+
기본 상태에서 방장이 임의로 변형하고 싶은 부분만 변경해서 사용 가능
142+
*/
143+
public static Room create(String title, String description, boolean isPrivate,
144+
String password, int maxParticipants, User creator, RoomTheme theme) {
145+
Room room = new Room();
146+
room.title = title;
147+
room.description = description;
148+
room.isPrivate = isPrivate;
149+
room.password = password;
150+
room.maxParticipants = maxParticipants;
151+
room.isActive = true; // 생성 시 기본적으로 활성화
152+
room.allowCamera = true; // 기본적으로 카메라 허용
153+
room.allowAudio = true; // 기본적으로 오디오 허용
154+
room.allowScreenShare = true; // 기본적으로 화면 공유 허용
155+
room.status = RoomStatus.WAITING; // 생성 시 대기 상태
156+
room.currentParticipants = 0; // 생성 시 참가자 0명
157+
room.createdBy = creator;
158+
room.theme = theme;
159+
160+
return room;
161+
}
162+
163+
/**
164+
* 방 설정 일괄 업데이트 메서드
165+
방장이 방 설정을 변경할 때 여러 필드를 한 번에 업데이트
166+
주된 생성 이유.. rtc 단체 제어를 위해 잡아놓았음. 잡아준 필드 변경 가능성 농후!!
167+
*/
168+
public void updateSettings(String title, String description, int maxParticipants,
169+
boolean allowCamera, boolean allowAudio, boolean allowScreenShare) {
170+
this.title = title;
171+
this.description = description;
172+
this.maxParticipants = maxParticipants;
173+
this.allowCamera = allowCamera;
174+
this.allowAudio = allowAudio;
175+
this.allowScreenShare = allowScreenShare;
176+
}
177+
178+
/**
179+
* 방 비밀번호 변경 메서드
180+
방장이 방의 비밀번호를 변경할 때
181+
별도 메서드로 분리한 이유: 비밀번호는 보안상 별도로 관리되어야 하기 때문 (ai의 추천)
182+
*/
183+
public void updatePassword(String newPassword) {
184+
this.password = newPassword;
185+
}
54186
}

src/main/java/com/back/domain/studyroom/entity/RoomMember.java

Lines changed: 170 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,22 +8,187 @@
88

99
import java.time.LocalDateTime;
1010

11+
/*
12+
RoomMember 엔티티 - 방과 사용자 간의 멤버십 관계를 나타냄
13+
연관관계 :
14+
- Room (1) : RoomMember (N) - 한 방에 여러 멤버가 있을 수 있음
15+
- User (1) : RoomMember (N) - 한 사용자가 여러 방의 멤버가 될 수 있음
16+
@JoinColumn vs @JoinTable 선택 이유:
17+
- @JoinColumn: 외래키를 이용한 직접 관계 (현재 변경)
18+
- @JoinTable: 별도의 연결 테이블을 만드는 관계
19+
RoomMember 테이블에서 그냥 room_id와 user_id 외래키로 직접 연결.
20+
*/
1121
@Entity
1222
@NoArgsConstructor
1323
@Getter
24+
@Table(uniqueConstraints = @UniqueConstraint(columnNames = {"room_id", "user_id"}))
1425
public class RoomMember extends BaseEntity {
26+
27+
// 방 정보 - 어떤 방의 멤버인지
1528
@ManyToOne(fetch = FetchType.LAZY)
16-
@JoinColumn(name = "room_id")
29+
@JoinColumn(name = "room_id") // room_member 테이블의 room_id 컬럼이 room 테이블의 id를 참조
1730
private Room room;
1831

32+
// 사용자 정보 - 누가 이 방의 멤버인지
1933
@ManyToOne(fetch = FetchType.LAZY)
20-
@JoinTable(name = "user_id")
34+
@JoinColumn(name = "user_id") // room_member 테이블의 user_id 컬럼이 users 테이블의 id를 참조
2135
private User user;
2236

37+
// 방 내에서의 역할 (방장, 부방장, 멤버, 방문객)
2338
@Enumerated(EnumType.STRING)
24-
private RoomRole role;
39+
@Column(nullable = false)
40+
private RoomRole role = RoomRole.VISITOR;
2541

26-
private LocalDateTime joinedAt;
42+
// 멤버십 기본 정보
43+
@Column(nullable = false)
44+
private LocalDateTime joinedAt; // 방에 처음 입장한 시간
45+
private LocalDateTime lastActiveAt; // 마지막으로 활동한 시간
2746

28-
private LocalDateTime lastActiveAt;
47+
// 실시간 상태 관리 필드들
48+
@Column(nullable = false)
49+
private boolean isOnline = false; // 현재 방에 온라인 상태인지
50+
51+
private String connectionId; // WebSocket 연결 ID (실시간 통신용)
52+
53+
private LocalDateTime lastHeartbeat; // 마지막 heartbeat 시간 (연결 상태 확인용)
54+
55+
// 💡 권한 확인 메서드들 (RoomRole enum의 메서드를 위임)
56+
57+
/**
58+
* 방 관리 권한이 있는지 확인 (방장, 부방장)
59+
방 설정 변경, 공지사항 작성 등의 권한이 필요할 때
60+
*/
61+
public boolean canManageRoom() {
62+
return role.canManageRoom();
63+
}
64+
65+
/**
66+
* 멤버 추방 권한이 있는지 확인 (방장, 부방장)
67+
다른 멤버를 추방하려고 할 때
68+
*/
69+
public boolean canKickMember() {
70+
return role.canKickMember();
71+
}
72+
73+
/**
74+
* 공지사항 관리 권한이 있는지 확인 (방장, 부방장)
75+
공지사항을 작성하거나 삭제할 때
76+
*/
77+
public boolean canManageNotices() {
78+
return role.canManageNotices();
79+
}
80+
81+
/**
82+
* 방장인지 확인
83+
방 소유자만 가능한 작업 (방 삭제, 호스트 권한 이양 등)
84+
*/
85+
public boolean isHost() {
86+
return role.isHost();
87+
}
88+
89+
/**
90+
* 정식 멤버인지 확인 (방문객이 아닌 멤버, 부방장, 방장)
91+
멤버만 접근 가능한 기능 (파일 업로드, 학습 기록 등)
92+
*/
93+
public boolean isMember() {
94+
return role.isMember();
95+
}
96+
97+
/**
98+
* 현재 활성 상태인지 확인
99+
온라인 멤버 목록 표시, 비활성 사용자 정리 등
100+
온라인 상태이고 최근 5분 이내에 heartbeat가 있었던 경우
101+
*/
102+
public boolean isActive() {
103+
return isOnline && lastHeartbeat != null &&
104+
lastHeartbeat.isAfter(LocalDateTime.now().minusMinutes(5));
105+
}
106+
107+
108+
/**
109+
기본 멤버 생성 메서드, 처음 입장 시 사용
110+
*/
111+
public static RoomMember create(Room room, User user, RoomRole role) {
112+
RoomMember member = new RoomMember();
113+
member.room = room;
114+
member.user = user;
115+
member.role = role;
116+
member.joinedAt = LocalDateTime.now();
117+
member.lastActiveAt = LocalDateTime.now();
118+
member.isOnline = true; // 생성 시 온라인 상태
119+
member.lastHeartbeat = LocalDateTime.now();
120+
121+
return member;
122+
}
123+
124+
// 방장 멤버 생성 -> 새로운 방을 생성할 때 방 생성자를 방장으로 등록
125+
public static RoomMember createHost(Room room, User user) {
126+
return create(room, user, RoomRole.HOST);
127+
}
128+
129+
/**
130+
* 일반 멤버 생성, 권한 자동 변경
131+
- 비공개 방에서 초대받은 사용자를 정식 멤버로 등록할 때 (로직 검토 중)
132+
*/
133+
public static RoomMember createMember(Room room, User user) {
134+
return create(room, user, RoomRole.MEMBER);
135+
}
136+
137+
/**
138+
* 방문객 생성
139+
* 사용 상황: 공개 방에 처음 입장하는 사용자를 임시 방문객으로 등록
140+
*/
141+
public static RoomMember createVisitor(Room room, User user) {
142+
return create(room, user, RoomRole.VISITOR);
143+
}
144+
145+
/**
146+
* 멤버의 역할 변경
147+
방장이 멤버를 부방장으로 승격시키거나 강등시킬 때
148+
*/
149+
public void updateRole(RoomRole newRole) {
150+
this.role = newRole;
151+
}
152+
153+
/**
154+
* 온라인 상태 변경
155+
* 사용 상황: 멤버가 방에 입장하거나 퇴장할 때
156+
활동 시간도 함께 업데이트, 온라인이 되면 heartbeat도 갱신
157+
*/
158+
public void updateOnlineStatus(boolean online) {
159+
this.isOnline = online;
160+
this.lastActiveAt = LocalDateTime.now();
161+
if (online) {
162+
this.lastHeartbeat = LocalDateTime.now();
163+
}
164+
}
165+
166+
/**
167+
* WebSocket 연결 ID 업데이트
168+
* 사용 상황: 멤버가 웹소켓으로 방에 연결될 때
169+
+ heartbeat도 함께 갱신
170+
*/
171+
public void updateConnectionId(String connectionId) {
172+
this.connectionId = connectionId;
173+
this.lastHeartbeat = LocalDateTime.now();
174+
}
175+
176+
/**
177+
* 사용 : 클라이언트에서 주기적으로 서버에 연결 상태를 알릴 때
178+
* 목적: 연결이 끊어진 멤버를 자동으로 감지하기 위해 사용, 별도의 다른 것으로 변경 가능
179+
*/
180+
public void heartbeat() {
181+
this.lastHeartbeat = LocalDateTime.now();
182+
this.lastActiveAt = LocalDateTime.now();
183+
this.isOnline = true;
184+
}
185+
186+
/**
187+
* 방 퇴장 처리 (명시적 퇴장과 연결 끊김 상태 로직 분할 예정임.. 일단은 임시로 통합 상태)
188+
멤버가 방을 나가거나 연결이 끊어졌을 때, 오프라인 상태로 변경하고 연결 ID 제거
189+
*/
190+
public void leave() {
191+
this.isOnline = false;
192+
this.connectionId = null;
193+
}
29194
}

0 commit comments

Comments
 (0)