Skip to content

Commit 7ccca97

Browse files
committed
Merge branch 'develop' into be-feature-#173
2 parents ee216da + 835f0bd commit 7ccca97

File tree

8 files changed

+214
-13
lines changed

8 files changed

+214
-13
lines changed

README.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
1-
![project-intro](https://github.com/user-attachments/assets/0df1909c-436d-4516-a639-78c4088e9871)
1+
![Sprint 33](https://github.com/user-attachments/assets/2b23184d-90ed-458d-9dc4-dab9579c1e48)
2+
23

34
> 배포 링크: http://octodocs.s3-website.kr.object.ncloudstorage.com/
45
>
56
7+
<br>
8+
9+
610

7-
<div align="center">
8-
9-
[![Hits](https://hits.seeyoufarm.com/api/count/incr/badge.svg?url=https%3A%2F%2Fgithub.com%2Fboostcampwm-2024%2Fweb15-OctoDocs&count_bg=%23FF9782&title_bg=%23231F20&icon=&icon_color=%23E7E7E7&title=views&edge_flat=false)](https://hits.seeyoufarm.com)
10-
11-
</div>
1211

13-
<div align="center">
1412

15-
[Project Wiki](https://github.com/boostcampwm-2024/web15-OctoDocs/wiki) | [Backlog](https://github.com/orgs/boostcampwm-2024/projects/120)
1613

14+
<div align="center">
15+
16+
![Hits](https://hits.seeyoufarm.com/api/count/incr/badge.svg?url=https%3A%2F%2Fgithub.com%2Fboostcampwm-2024%2Fweb15-OctoDocs&count_bg=%23000000&title_bg=%23000000&icon=&icon_color=%23E7E7E7&title=hits&edge_flat=false) [![Group 83 (2)](https://github.com/user-attachments/assets/2d106d94-430c-47bc-a9e2-1f0026f76c2f)](https://github.com/boostcampwm-2024/web15-OctoDocs/wiki) [![Group 84 (2)](https://github.com/user-attachments/assets/b29b191b-8172-42a9-b541-40fdb8f165f3)](https://github.com/orgs/boostcampwm-2024/projects/120)
1717

1818
</div>
1919

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { Panel } from "@xyflow/react";
2+
import { useReactFlow } from "@xyflow/react";
3+
import { type AwarenessState } from "@/hooks/useCursor";
4+
import Cursor from "./cursor";
5+
import { useMemo } from "react";
6+
7+
interface CollaborativeCursorsProps {
8+
cursors: Map<number, AwarenessState>;
9+
}
10+
11+
export function CollaborativeCursors({ cursors }: CollaborativeCursorsProps) {
12+
const { flowToScreenPosition } = useReactFlow();
13+
14+
const validCursors = useMemo(
15+
() => Array.from(cursors.values()).filter((cursor) => cursor.cursor),
16+
[cursors],
17+
);
18+
19+
return (
20+
<Panel>
21+
{validCursors.map((cursor) => (
22+
<Cursor
23+
key={cursor.clientId}
24+
coors={flowToScreenPosition({
25+
x: cursor.cursor!.x,
26+
y: cursor.cursor!.y,
27+
})}
28+
color={cursor.color}
29+
/>
30+
))}
31+
</Panel>
32+
);
33+
}

apps/frontend/src/components/canvas/index.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ import { cn } from "@/lib/utils";
2525
import { useQueryClient } from "@tanstack/react-query";
2626
import useYDocStore from "@/store/useYDocStore";
2727

28+
import { useCollaborativeCursors } from "@/hooks/useCursor";
29+
import { CollaborativeCursors } from "../CursorView";
30+
2831
const proOptions = { hideAttribution: true };
2932

3033
interface CanvasProps {
@@ -39,6 +42,12 @@ function Flow({ className }: CanvasProps) {
3942

4043
const { ydoc } = useYDocStore();
4144

45+
const { cursors, handleMouseMove, handleNodeDrag, handleMouseLeave } =
46+
useCollaborativeCursors({
47+
ydoc,
48+
roomName: "flow-room",
49+
});
50+
4251
const provider = useRef<WebsocketProvider>();
4352
const existingPageIds = useRef(new Set<string>());
4453

@@ -214,12 +223,14 @@ function Flow({ className }: CanvasProps) {
214223
const nodeTypes = useMemo(() => ({ note: NoteNode }), []);
215224

216225
return (
217-
<div className={cn("", className)}>
226+
<div className={cn("", className)} onMouseMove={handleMouseMove}>
218227
<ReactFlow
219228
nodes={nodes}
220229
edges={edges}
221230
onNodesChange={handleNodesChange}
222231
onEdgesChange={handleEdgesChange}
232+
onMouseLeave={handleMouseLeave}
233+
onNodeDrag={handleNodeDrag}
223234
onConnect={onConnect}
224235
proOptions={proOptions}
225236
nodeTypes={nodeTypes}
@@ -229,6 +240,7 @@ function Flow({ className }: CanvasProps) {
229240
<Controls />
230241
<MiniMap />
231242
<Background variant={BackgroundVariant.Dots} gap={12} size={1} />
243+
<CollaborativeCursors cursors={cursors} />
232244
</ReactFlow>
233245
</div>
234246
);

apps/frontend/src/components/cursor/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export default function Cursor({ coors, color = "#ffb8b9" }: CursorProps) {
3232
d="M2.40255 5.31234C1.90848 3.6645 3.58743 2.20312 5.15139 2.91972L90.0649 41.8264C91.7151 42.5825 91.5858 44.9688 89.8637 45.5422L54.7989 57.2186C53.3211 57.7107 52.0926 58.7582 51.3731 60.1397L33.0019 95.4124C32.1726 97.0047 29.8279 96.7826 29.3124 95.063L2.40255 5.31234Z"
3333
fill={color}
3434
stroke="black"
35-
stroke-width="4"
35+
strokeWidth="4"
3636
/>
3737
</svg>
3838
</motion.div>

apps/frontend/src/components/editor/EditorTitle.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export default function EditorTitle({
3232
<input
3333
type="text"
3434
value={input as string}
35-
className="w-full text-xl font-bold outline-none"
35+
className="w-full text-4xl font-bold outline-none"
3636
onChange={handleTitleChange}
3737
/>
3838
</div>

apps/frontend/src/components/editor/prosemirror.css

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,44 @@
1+
.tiptap p {
2+
margin-top: 0;
3+
margin-bottom: 0;
4+
padding: 3px 2px;
5+
line-height: 1.5;
6+
font-size: 16px;
7+
}
8+
9+
.tiptap h1 {
10+
margin-top: 2rem;
11+
margin-bottom: 2px;
12+
padding: 3px 2px;
13+
line-height: 1.3;
14+
font-size: 1.875rem;
15+
font-weight: 600;
16+
}
17+
18+
.tiptap h2 {
19+
margin-top: 1.4rem;
20+
margin-bottom: 1px;
21+
padding: 3px 2px;
22+
line-height: 1.3;
23+
font-size: 1.5rem;
24+
font-weight: 600;
25+
}
26+
27+
.tiptap h3 {
28+
margin-top: 1rem;
29+
margin-bottom: 1px;
30+
padding: 3px 2px;
31+
line-height: 1.3;
32+
font-size: 1.25rem;
33+
font-weight: 600;
34+
}
35+
136
.ProseMirror {
2-
@apply p-12 px-8 sm:px-12;
37+
@apply p-8 sm:px-12;
38+
}
39+
40+
.ProseMirror .p {
41+
@apply my-0;
342
}
443

544
.ProseMirror .is-editor-empty:first-child::before {

apps/frontend/src/components/layout/EditorLayout.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ const EditorLayout = ({ children }: EditorLayoutProps) => {
1010

1111
return (
1212
<div
13-
className={`absolute right-0 h-[720px] w-[520px] transform rounded-bl-lg rounded-br-lg rounded-tr-lg border bg-white shadow-lg transition-transform duration-100 ease-in-out ${isPanelOpen ? "translate-x-0" : "translate-x-full"}`}
13+
className={`absolute right-0 h-[720px] w-[520px] rounded-bl-lg rounded-br-lg rounded-tr-lg border bg-white shadow-lg transition-transform duration-100 ease-in-out ${
14+
isPanelOpen ? "transform-none" : "translate-x-full"
15+
}`}
1416
>
1517
<div className="h-full overflow-auto">{children}</div>
1618

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import { useReactFlow, type XYPosition } from "@xyflow/react";
2+
import * as Y from "yjs";
3+
import { useCallback, useEffect, useRef, useState } from "react";
4+
import { WebsocketProvider } from "y-websocket";
5+
6+
const CURSOR_COLORS = [
7+
"#7d7b94",
8+
"#41c76d",
9+
"#f86e7e",
10+
"#f6b8b8",
11+
"#f7d353",
12+
"#3b5bf7",
13+
"#59cbf7",
14+
] as const;
15+
16+
export interface AwarenessState {
17+
cursor: XYPosition | null;
18+
color: string;
19+
clientId: number;
20+
}
21+
22+
interface CollaborativeCursorsProps {
23+
ydoc: Y.Doc;
24+
roomName?: string;
25+
}
26+
27+
export function useCollaborativeCursors({
28+
ydoc,
29+
roomName = "cursor-room",
30+
}: CollaborativeCursorsProps) {
31+
const flowInstance = useReactFlow();
32+
const provider = useRef<WebsocketProvider>();
33+
const [cursors, setCursors] = useState<Map<number, AwarenessState>>(
34+
new Map(),
35+
);
36+
const clientId = useRef<number | null>(null);
37+
const userColor = useRef(
38+
CURSOR_COLORS[Math.floor(Math.random() * CURSOR_COLORS.length)],
39+
);
40+
41+
useEffect(() => {
42+
const wsProvider = new WebsocketProvider(
43+
import.meta.env.VITE_WS_URL,
44+
roomName,
45+
ydoc,
46+
);
47+
48+
provider.current = wsProvider;
49+
clientId.current = wsProvider.awareness.clientID;
50+
51+
wsProvider.awareness.setLocalState({
52+
cursor: null,
53+
color: userColor.current,
54+
clientId: wsProvider.awareness.clientID,
55+
});
56+
57+
wsProvider.awareness.on("change", () => {
58+
const states = new Map(
59+
Array.from(
60+
wsProvider.awareness.getStates() as Map<number, AwarenessState>,
61+
).filter(
62+
([key, state]) => key !== clientId.current && state.cursor !== null,
63+
),
64+
);
65+
setCursors(states);
66+
});
67+
68+
return () => {
69+
wsProvider.destroy();
70+
};
71+
}, [ydoc, roomName]);
72+
73+
const updateCursorPosition = useCallback(
74+
(x: number | null, y: number | null) => {
75+
if (!provider.current?.awareness) return;
76+
77+
const cursor =
78+
x !== null && y !== null
79+
? flowInstance?.screenToFlowPosition({ x, y })
80+
: null;
81+
82+
provider.current.awareness.setLocalState({
83+
cursor,
84+
color: userColor.current,
85+
clientId: provider.current.awareness.clientID,
86+
});
87+
},
88+
[flowInstance],
89+
);
90+
91+
const handleMouseMove = useCallback(
92+
(e: React.MouseEvent) => {
93+
updateCursorPosition(e.clientX, e.clientY);
94+
},
95+
[updateCursorPosition],
96+
);
97+
98+
const handleNodeDrag = useCallback(
99+
(e: React.MouseEvent) => {
100+
updateCursorPosition(e.clientX, e.clientY);
101+
},
102+
[updateCursorPosition],
103+
);
104+
105+
const handleMouseLeave = useCallback(() => {
106+
updateCursorPosition(null, null);
107+
}, [updateCursorPosition]);
108+
109+
return {
110+
cursors,
111+
handleMouseMove,
112+
handleNodeDrag,
113+
handleMouseLeave,
114+
};
115+
}

0 commit comments

Comments
 (0)