Skip to content

Commit fa4c8b1

Browse files
committed
Feat: 참여자 실시간 화면 구현
1 parent 67ae9a4 commit fa4c8b1

5 files changed

Lines changed: 329 additions & 99 deletions

File tree

src/api/main/projectView.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
import Send from "@/api/send.ts";
22

3+
export const getStatus = (projectID: string) => {
4+
return Send({
5+
method: "get",
6+
url: `/main/project/${projectID}/status`,
7+
});
8+
};
9+
10+
311
export const getProjectView = (projectID: string) => {
412
return Send({
513
method: "get",
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
import React from "react";
2+
import {
3+
ReactFlow,
4+
useNodesState,
5+
useEdgesState,
6+
Node,
7+
Edge,
8+
} from "@xyflow/react";
9+
10+
// style
11+
import "@xyflow/react/dist/style.css";
12+
import "@/views/meeting/style/mind-map.sass";
13+
14+
// api
15+
import { getProfile } from "@/api/main/profile";
16+
17+
// import
18+
import { useEffect, useState, useRef } from "react";
19+
import { Client } from "@stomp/stompjs";
20+
import toltip from "@/assets/imgs/icon/toltip.svg";
21+
// type
22+
import { RealTimeSummaryData } from "@/types/realTimeSummaryData";
23+
24+
interface scriptData {
25+
time: string;
26+
script: string;
27+
}
28+
29+
interface MindMapViewProps {
30+
setScripts: React.Dispatch<React.SetStateAction<scriptData[]>>;
31+
projectId: string;
32+
setSummary: React.Dispatch<React.SetStateAction<RealTimeSummaryData[]>>;
33+
}
34+
35+
const MindMapView = ({
36+
setScripts,
37+
projectId,
38+
setSummary,
39+
}: MindMapViewProps) => {
40+
const [mainKeyword, setMainKeyword] = useState([]);
41+
const [recommendKeyword, setRecommendKeyword] = useState([]);
42+
const mapRef = useRef<HTMLDivElement>(null);
43+
const keywordRef = useRef<HTMLDivElement>(null);
44+
45+
const clientRef = useRef<Client | null>(null); // WebSocket 클라이언트 저장
46+
47+
useEffect(() => {
48+
const client = new Client({
49+
brokerURL: import.meta.env.VITE_API_WS_URL,
50+
reconnectDelay: 5000,
51+
debug: (str) => {
52+
console.log(str);
53+
},
54+
onConnect: () => {
55+
client.subscribe(
56+
`/topic/conference/${projectId}`,
57+
(message: any) => {
58+
const data: any = JSON.parse(message.body);
59+
console.log(data);
60+
61+
if (data.event === "create_node") {
62+
setInitialNodes(data.nodes);
63+
64+
const edges = data.nodes
65+
.filter((node: any) => node.parentId)
66+
.map((node: any, index: number) => ({
67+
id: `${index}`,
68+
source: node.parentId!,
69+
target: node.id,
70+
}));
71+
72+
setInitialEdges(edges);
73+
}
74+
75+
if (data.event === "summary") {
76+
setSummary((prev) => [
77+
...prev,
78+
{
79+
time: data.time,
80+
title: data.title,
81+
item: data.content,
82+
},
83+
]);
84+
}
85+
86+
if (data.event === "script") {
87+
console.log(data.scription)
88+
setScripts((prev) => [...prev, data.scription]);
89+
}
90+
91+
if (data.event === "main_keywords") {
92+
setMainKeyword(
93+
data.keywords.map((x: any, i: number) => ({ id: i, value: x }))
94+
);
95+
}
96+
97+
if (data.event === "recommended_keywords") {
98+
setRecommendKeyword(
99+
data.keywords.map((x: any, i: number) => ({ id: i, value: x }))
100+
);
101+
}
102+
}
103+
);
104+
105+
getProfile().then((res: any) => {
106+
client.publish({
107+
destination: `/app/conference/${projectId}/modify_inviting`,
108+
body: JSON.stringify({
109+
event: "participant_join",
110+
projectId: projectId,
111+
memberId: res.data.data.memberId,
112+
}),
113+
});
114+
});
115+
},
116+
onWebSocketError: (event) => {
117+
console.error("❌ WebSocket 연결 실패:", event);
118+
},
119+
onStompError: (frame) => {
120+
console.error("❌ STOMP 에러:", frame);
121+
},
122+
});
123+
client.activate();
124+
clientRef.current = client;
125+
126+
}, []);
127+
128+
const [initialNodes, setInitialNodes] = useState<Node[]>([
129+
{
130+
id: "1",
131+
type: "input",
132+
data: { label: "회의 키워드" },
133+
position: { x: -150, y: 0 },
134+
},
135+
{
136+
id: "2",
137+
type: "output",
138+
data: { label: "다음 키워드" },
139+
140+
position: { x: 150, y: 0 },
141+
},
142+
]);
143+
144+
const [initialEdges, setInitialEdges] = useState<Edge[]>([
145+
{ id: "1", source: "1", target: "2" },
146+
]);
147+
148+
const [nodes, setNodes] = useNodesState(initialNodes);
149+
const [edges, setEdges] = useEdgesState(initialEdges);
150+
151+
useEffect(() => {
152+
setNodes(initialNodes);
153+
setEdges(initialEdges);
154+
}, [initialNodes, initialEdges]); // 임시방편으로 만듦
155+
156+
return (
157+
<div className="mind-map-container">
158+
<div className="mind-map-main">
159+
<div className="mind-map-wrap" ref={mapRef}>
160+
<ReactFlow
161+
nodes={nodes}
162+
edges={edges}
163+
fitView
164+
attributionPosition="top-right"
165+
></ReactFlow>
166+
</div>
167+
<div className="keyword-container" ref={keywordRef}>
168+
<h2>라이브 키워드</h2>
169+
<div className="main-keyword-wrap">
170+
<h3>
171+
주요 키워드
172+
<div className="toltip-wrap">
173+
<img src={toltip} alt="" />
174+
<div className="toltip">
175+
녹음 중 자동으로 추출된 핵심 키워드예요. 대화에서 자주
176+
언급되는 단어나 중요한 개념을 실시간으로 분석해
177+
제공해요.
178+
</div>
179+
</div>
180+
</h3>
181+
{mainKeyword.length ? (
182+
<div className="keyword-wrap">
183+
{mainKeyword.map((x: any) => (
184+
<div
185+
className="keyword"
186+
id={x.id.toString()}
187+
key={x.id}
188+
>
189+
{x.value}
190+
</div>
191+
))}
192+
</div>
193+
) : (
194+
<p>회의가 진행되면 주요 키워드가 자동 생성됩니다.</p>
195+
)}
196+
</div>
197+
<div className="recommend-keyword-wrap">
198+
<h3>
199+
추천 키워드
200+
<div className="toltip-wrap">
201+
<img src={toltip} alt="" />
202+
<div className="toltip">
203+
마인드맵에 추가하면 좋을 키워드를 추천해요. 대화의
204+
흐름과 맥락을 분석해 관련성이 높은 키워드를 제안해요.
205+
</div>
206+
</div>
207+
</h3>
208+
{recommendKeyword.length ? (
209+
<div className="keyword-wrap">
210+
{recommendKeyword.map((x: any) => (
211+
<div
212+
className="keyword"
213+
id={x.id.toString()}
214+
key={x.id}
215+
>
216+
{x.value}
217+
</div>
218+
))}
219+
</div>
220+
) : (
221+
<p>회의가 진행되면 추천 키워드가 자동 생성됩니다.</p>
222+
)}
223+
</div>
224+
</div>
225+
</div>
226+
</div>
227+
);
228+
};
229+
230+
export default MindMapView;

0 commit comments

Comments
 (0)