Skip to content

Commit 207bdf1

Browse files
authored
Merge pull request #93 from DMU-DebugVisual/sojeong
코드방송 UI 수정, 웹소켓 연결
2 parents dfe3039 + 0605235 commit 207bdf1

19 files changed

+1161
-249
lines changed

package-lock.json

Lines changed: 45 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"private": true,
66
"dependencies": {
77
"@monaco-editor/react": "^4.7.0",
8+
"@stomp/stompjs": "^7.2.1",
89
"@testing-library/dom": "^10.4.0",
910
"@testing-library/jest-dom": "^6.6.3",
1011
"@testing-library/react": "^16.3.0",
@@ -18,6 +19,7 @@
1819
"react-router-dom": "^7.5.3",
1920
"react-scripts": "5.0.1",
2021
"react-scroll": "^1.9.3",
22+
"sockjs-client": "^1.6.1",
2123
"web-vitals": "^2.1.4"
2224
},
2325
"scripts": {

src/components/codecast/Codecast.css

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
2-
31
.broadcast-container {
42
background-color: #ffffff;
53
min-height: 100vh;
@@ -11,7 +9,6 @@
119
text-align: center;
1210
}
1311

14-
1512
.broadcast-header {
1613
margin-bottom: 30px;
1714
}
@@ -83,6 +80,12 @@
8380
font-weight: bold;
8481
}
8582

83+
.error-text {
84+
color: #d32f2f;
85+
font-size: 13px;
86+
margin-top: 6px;
87+
}
88+
8689
.start-btn {
8790
width: 100%;
8891
color: white;

src/components/codecast/Codecast.jsx

Lines changed: 37 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,56 @@
1-
import React from "react";
1+
// Codecast.jsx
2+
import React, { useState } from "react";
23
import { FaPlus, FaArrowRight, FaDesktop } from "react-icons/fa";
34
import "./Codecast.css";
45
import { Link, useNavigate } from "react-router-dom";
56

67
const Codecast = () => {
78
const navigate = useNavigate();
9+
const [inviteCode, setInviteCode] = useState("");
810

911
const handleJoin = () => {
10-
// 나중에 코드 검증 로직 추가 가능
11-
navigate("/broadcast/live");
12+
const code = inviteCode.trim();
13+
if (!code) return alert("방송 코드를 입력하세요.");
14+
// ✅ rid 쿼리로 전달
15+
navigate(`/broadcast/live?rid=${encodeURIComponent(code)}`);
1216
};
1317

1418
return (
15-
<div>
16-
<section className="broadcast-container">
17-
<div className="broadcast-header">
18-
<FaDesktop className="broadcast-icon" />
19-
<h2>코드 방송</h2>
20-
<p>
21-
실시간으로 코드를 공유하고 함께 학습하세요.<br />
22-
방송 코드를 입력하거나 직접 방송을 시작해보세요.
23-
</p>
24-
</div>
19+
<section className="broadcast-container">
20+
<div className="broadcast-header">
21+
<FaDesktop className="broadcast-icon" />
22+
<h2>코드 방송</h2>
23+
<p>실시간으로 코드를 공유하고 함께 학습하세요.</p>
24+
</div>
2525

26-
<div className="broadcast-card">
27-
<div className="broadcast-join">
28-
<h3>방송 참여하기</h3>
29-
<p>방송 코드를 입력하여 진행 중인 방송에 참여하세요.</p>
30-
<div className="input-group">
31-
<input type="text" placeholder="방송 코드 입력" />
32-
<button onClick={handleJoin}>
33-
<FaArrowRight />
34-
참여하기
35-
</button>
36-
</div>
26+
<div className="broadcast-card">
27+
<div className="broadcast-join">
28+
<h3>방송 참여하기</h3>
29+
<p>방송 코드를 입력하여 진행 중인 방송에 참여하세요.</p>
30+
<div className="input-group">
31+
<input
32+
type="text"
33+
placeholder="방송 코드 입력"
34+
value={inviteCode}
35+
onChange={(e) => setInviteCode(e.target.value)}
36+
/>
37+
<button onClick={handleJoin}>
38+
<FaArrowRight />
39+
참여하기
40+
</button>
3741
</div>
42+
</div>
3843

39-
<div className="hr-with-text"><span>또는</span></div>
44+
<div className="hr-with-text"><span>또는</span></div>
4045

41-
<div className="broadcast-start">
42-
<h3>새 방송 시작하기</h3>
43-
<p>새로운 코드 방송을 시작하여 다른 사용자들과 실시간으로 코드를 공유하세요.</p>
44-
<Link to="/startbroadcast" className="start-btn">
45-
<FaPlus />
46-
새 방송 시작
47-
</Link>
48-
</div>
46+
<div className="broadcast-start">
47+
<h3>새 방송 시작하기</h3>
48+
<Link to="/startbroadcast" className="start-btn">
49+
<FaPlus /> 새 방송 시작
50+
</Link>
4951
</div>
50-
</section>
51-
</div>
52+
</div>
53+
</section>
5254
);
5355
};
5456

src/components/codecast/StartCodecast.jsx

Lines changed: 65 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,62 @@
1-
import React from "react";
1+
// StartCodecast.jsx
2+
import React, { useState } from "react";
23
import { FaArrowRight } from "react-icons/fa";
34
import "./Codecast.css";
5+
import { useNavigate } from "react-router-dom";
6+
import { createRoom } from "./api/collab";
47

58
const StartCodecast = () => {
9+
const navigate = useNavigate();
10+
const [roomName, setRoomName] = useState("");
11+
const [roomCode, setRoomCode] = useState("");
12+
const [loading, setLoading] = useState(false);
13+
const [errMsg, setErrMsg] = useState("");
14+
15+
const handleCreate = async () => {
16+
setErrMsg("");
17+
18+
const token = localStorage.getItem("token");
19+
const username = localStorage.getItem("username");
20+
if (!token || !username) {
21+
setErrMsg("로그인이 필요합니다. 먼저 로그인해주세요.");
22+
return;
23+
}
24+
if (!roomName.trim()) {
25+
setErrMsg("방 이름을 입력해주세요.");
26+
return;
27+
}
28+
29+
try {
30+
setLoading(true);
31+
const data = await createRoom({
32+
token,
33+
name: roomName.trim(),
34+
// code: roomCode.trim() || undefined,
35+
});
36+
37+
const roomId = data.roomId;
38+
// ✅ 쿼리스트링 rid로 입장 + state로 보조 정보 전달
39+
navigate(`/broadcast/live?rid=${encodeURIComponent(roomId)}`, {
40+
state: {
41+
roomId,
42+
title: data.roomName || roomName.trim(),
43+
defaultSessionId: data.defaultSessionId || null,
44+
ownerId: data.ownerId,
45+
accessCode: roomCode.trim() || null,
46+
},
47+
});
48+
} catch (err) {
49+
console.error(err);
50+
setErrMsg(
51+
err.status === 401
52+
? "로그인이 만료되었습니다. 다시 로그인해주세요."
53+
: err.message || "방 생성 중 오류가 발생했습니다."
54+
);
55+
} finally {
56+
setLoading(false);
57+
}
58+
};
59+
660
return (
761
<div>
862
<section className="broadcast-container">
@@ -20,23 +74,27 @@ const StartCodecast = () => {
2074
type="text"
2175
id="roomName"
2276
placeholder="방 이름을 입력하세요"
77+
value={roomName}
78+
onChange={(e) => setRoomName(e.target.value)}
2379
/>
2480
</div>
2581

2682
<div className="start-input-group">
27-
<label>방송 코드</label>
83+
<label>방송 코드 (선택)</label>
2884
<input
2985
type="text"
3086
id="roomCode"
3187
placeholder="방송 코드를 입력하세요 (선택사항)"
88+
value={roomCode}
89+
onChange={(e) => setRoomCode(e.target.value)}
3290
/>
33-
<p className="help-text">
34-
방송 코드는 다른 사용자가 방송에 참여할 때 사용됩니다.
35-
</p>
91+
<p className="help-text">방송 코드는 다른 사용자가 방송에 참여할 때 사용됩니다.</p>
3692
</div>
3793

38-
<button className="start-btn">
39-
방송 시작하기 <FaArrowRight />
94+
{errMsg && <p className="error-text">{errMsg}</p>}
95+
96+
<button className="start-btn" onClick={handleCreate} disabled={loading}>
97+
{loading ? "생성 중..." : <>방송 시작하기 <FaArrowRight /></>}
4098
</button>
4199
</div>
42100
</section>
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import config from "../../../config";
2+
3+
export async function createRoom({ token, name /*, code*/ }) {
4+
if (!token) throw new Error("NO_TOKEN");
5+
6+
// const url = `${config.API_BASE_URL}/api/collab/rooms`;
7+
const url = `http://52.79.145.160:8080/api/collab/rooms`;
8+
9+
// 스웨거 기준 roomName만 전송
10+
const body = { roomName: name };
11+
12+
const res = await fetch(url, {
13+
method: "POST",
14+
headers: {
15+
"Content-Type": "application/json",
16+
Authorization: `Bearer ${token}`,
17+
},
18+
body: JSON.stringify(body),
19+
});
20+
21+
if (!res.ok) {
22+
const text = await res.text().catch(() => "");
23+
const err = new Error(`HTTP ${res.status}: ${text || res.statusText}`);
24+
err.status = res.status;
25+
throw err;
26+
}
27+
28+
// { roomId, roomName, ownerId, defaultSessionId }
29+
return res.json();
30+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// src/components/codecast/api/roomAdmin.js
2+
const API_BASE = process.env.REACT_APP_API_BASE_URL || 'http://52.79.145.160:8080';
3+
4+
async function req(path, { method = 'GET', token, body } = {}) {
5+
const res = await fetch(`${API_BASE}${path}`, {
6+
method,
7+
headers: {
8+
'Content-Type': 'application/json',
9+
...(token ? { Authorization: `Bearer ${token}` } : {}),
10+
},
11+
credentials: 'include',
12+
body: body ? JSON.stringify(body) : undefined,
13+
});
14+
if (!res.ok) {
15+
const text = await res.text().catch(() => '');
16+
throw new Error(text || `HTTP ${res.status}`);
17+
}
18+
try { return await res.json(); } catch { return {}; }
19+
}
20+
21+
/** 방에서 참가자 강퇴 */
22+
export function kickParticipant({ token, roomId, targetUserId }) {
23+
return req(`/api/collab/rooms/${encodeURIComponent(roomId)}/participants/${encodeURIComponent(targetUserId)}`, {
24+
method: 'DELETE',
25+
token,
26+
});
27+
}
28+
29+
/** 세션 내 쓰기 권한 부여 (edit) */
30+
export function grantEditPermission({ token, sessionId, targetUserId }) {
31+
return req(`/api/collab/sessions/${encodeURIComponent(sessionId)}/permissions/${encodeURIComponent(targetUserId)}`, {
32+
method: 'POST',
33+
token,
34+
});
35+
}
36+
37+
/** 세션 내 쓰기 권한 회수 (view) */
38+
export function revokeEditPermission({ token, sessionId, targetUserId }) {
39+
return req(`/api/collab/sessions/${encodeURIComponent(sessionId)}/permissions/${encodeURIComponent(targetUserId)}`, {
40+
method: 'DELETE',
41+
token,
42+
});
43+
}

0 commit comments

Comments
 (0)