Skip to content

Commit

Permalink
Merge pull request #210 from boostcampwm-2024/feature-fe-#206
Browse files Browse the repository at this point in the history
유저를 관리하기 위한 웹소켓 Room 추가
  • Loading branch information
djk01281 authored Nov 19, 2024
2 parents 0772c25 + d489d0c commit 2ddd6ac
Show file tree
Hide file tree
Showing 10 changed files with 200 additions and 26 deletions.
5 changes: 5 additions & 0 deletions apps/frontend/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";

import Sidebar from "./components/sidebar";
import HoverTrigger from "./components/HoverTrigger";
import EditorView from "./components/EditorView";
import SideWrapper from "./components/layout/SideWrapper";
import Canvas from "./components/canvas";
import ScrollWrapper from "./components/layout/ScrollWrapper";

import { useSyncedUsers } from "./hooks/useSyncedUsers";

const queryClient = new QueryClient();

function App() {
useSyncedUsers();

return (
<QueryClientProvider client={queryClient}>
<div className="fixed inset-0 bg-white">
Expand Down
11 changes: 10 additions & 1 deletion apps/frontend/src/components/CursorView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useReactFlow } from "@xyflow/react";
import { type AwarenessState } from "@/hooks/useCursor";
import Cursor from "./cursor";
import { useMemo } from "react";
import useUserStore from "@/store/useUserStore";

interface CollaborativeCursorsProps {
cursors: Map<number, AwarenessState>;
Expand All @@ -11,8 +12,15 @@ interface CollaborativeCursorsProps {
export function CollaborativeCursors({ cursors }: CollaborativeCursorsProps) {
const { flowToScreenPosition } = useReactFlow();

const { currentUser } = useUserStore();

const validCursors = useMemo(
() => Array.from(cursors.values()).filter((cursor) => cursor.cursor),
() =>
Array.from(cursors.values()).filter(
(cursor) =>
cursor.cursor &&
(cursor.clientId as unknown as string) !== currentUser.clientId,
),
[cursors],
);

Expand All @@ -26,6 +34,7 @@ export function CollaborativeCursors({ cursors }: CollaborativeCursorsProps) {
y: cursor.cursor!.y,
})}
color={cursor.color}
clientId={cursor.clientId.toString()}
/>
))}
</Panel>
Expand Down
14 changes: 12 additions & 2 deletions apps/frontend/src/components/EditorView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,22 @@ import * as Y from "yjs";
import { SocketIOProvider } from "y-socket.io";

import Editor from "./editor";
import usePageStore from "@/store/usePageStore";
import { usePage, useUpdatePage } from "@/hooks/usePages";
import EditorLayout from "./layout/EditorLayout";
import EditorTitle from "./editor/EditorTitle";
import SaveStatus from "./editor/ui/SaveStatus";
import ActiveUser from "./commons/activeUser";

import usePageStore from "@/store/usePageStore";
import useUserStore from "@/store/useUserStore";
import { usePage, useUpdatePage } from "@/hooks/usePages";

export default function EditorView() {
const { currentPage } = usePageStore();
const { page, isLoading } = usePage(currentPage);
const [saveStatus, setSaveStatus] = useState<"saved" | "unsaved">("saved");
const [ydoc, setYDoc] = useState<Y.Doc | null>(null);
const [provider, setProvider] = useState<SocketIOProvider | null>(null);
const { users } = useUserStore();

useEffect(() => {
if (!currentPage) return;
Expand Down Expand Up @@ -88,6 +92,12 @@ export default function EditorView() {
currentPage={currentPage}
pageContent={pageContent}
/>
<ActiveUser
className="px-12 py-4"
users={users.filter(
(user) => user.currentPageId === currentPage.toString(),
)}
/>
<Editor
key={ydoc.guid}
initialContent={pageContent}
Expand Down
11 changes: 11 additions & 0 deletions apps/frontend/src/components/canvas/NoteNode.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import { Handle, NodeProps, Position, type Node } from "@xyflow/react";

import ActiveUser from "../commons/activeUser";

import usePageStore from "@/store/usePageStore";
import useUserStore from "@/store/useUserStore";

export type NoteNodeData = { title: string; id: number };
export type NoteNodeType = Node<NoteNodeData, "note">;

export function NoteNode({ data }: NodeProps<NoteNodeType>) {
const { setCurrentPage } = usePageStore();
const { users } = useUserStore();

const handleNodeClick = () => {
const id = data.id;
Expand Down Expand Up @@ -46,6 +51,12 @@ export function NoteNode({ data }: NodeProps<NoteNodeType>) {
isConnectable={true}
/>
{data.title}
<ActiveUser
className="justify-end"
users={users.filter(
(user) => user.currentPageId === data.id.toString(),
)}
/>
</div>
);
}
28 changes: 28 additions & 0 deletions apps/frontend/src/components/commons/activeUser/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { User } from "@/store/useUserStore";
import { cn } from "@/lib/utils";

interface ActiveUserProps {
users: User[];
className?: string;
}

export default function ActiveUser({ users, className }: ActiveUserProps) {
return (
<div className={cn("flex gap-2", className)}>
{users.map((user) => (
<div
style={{ backgroundColor: user.color }}
className={cn("group relative h-5 w-5 rounded-full")}
key={user.clientId}
>
<div
style={{ backgroundColor: user.color }}
className="absolute left-2 z-10 hidden px-2 text-sm group-hover:flex"
>
{user.clientId}
</div>
</div>
))}
</div>
);
}
10 changes: 9 additions & 1 deletion apps/frontend/src/components/cursor/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,15 @@ export interface Coors {

interface CursorProps {
coors: Coors;
clientId: string;
color?: string;
}

export default function Cursor({ coors, color = "#ffb8b9" }: CursorProps) {
export default function Cursor({
coors,
color = "#ffb8b9",
clientId,
}: CursorProps) {
const { x, y } = coors;

return (
Expand All @@ -35,6 +40,9 @@ export default function Cursor({ coors, color = "#ffb8b9" }: CursorProps) {
strokeWidth="4"
/>
</svg>
<p className="absolute px-1" style={{ backgroundColor: color }}>
{clientId}
</p>
</motion.div>
);
}
29 changes: 7 additions & 22 deletions apps/frontend/src/hooks/useCursor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,7 @@ import { useReactFlow, type XYPosition } from "@xyflow/react";
import * as Y from "yjs";
import { useCallback, useEffect, useRef, useState } from "react";
import { SocketIOProvider } from "y-socket.io";

const CURSOR_COLORS = [
"#7d7b94",
"#41c76d",
"#f86e7e",
"#f6b8b8",
"#f7d353",
"#3b5bf7",
"#59cbf7",
] as const;
import useUserStore from "@/store/useUserStore";

export interface AwarenessState {
cursor: XYPosition | null;
Expand All @@ -33,10 +24,7 @@ export function useCollaborativeCursors({
const [cursors, setCursors] = useState<Map<number, AwarenessState>>(
new Map(),
);
const clientId = useRef<number | null>(null);
const userColor = useRef(
CURSOR_COLORS[Math.floor(Math.random() * CURSOR_COLORS.length)],
);
const { currentUser } = useUserStore();

useEffect(() => {
const wsProvider = new SocketIOProvider(
Expand All @@ -55,21 +43,18 @@ export function useCollaborativeCursors({
);

provider.current = wsProvider;
clientId.current = wsProvider.awareness.clientID;

wsProvider.awareness.setLocalState({
cursor: null,
color: userColor.current,
clientId: wsProvider.awareness.clientID,
color: currentUser.color,
clientId: currentUser.clientId,
});

wsProvider.awareness.on("change", () => {
const states = new Map(
Array.from(
wsProvider.awareness.getStates() as Map<number, AwarenessState>,
).filter(
([key, state]) => key !== clientId.current && state.cursor !== null,
),
).filter(([, state]) => state.cursor !== null),
);
setCursors(states);
});
Expand All @@ -90,8 +75,8 @@ export function useCollaborativeCursors({

provider.current.awareness.setLocalState({
cursor,
color: userColor.current,
clientId: provider.current.awareness.clientID,
color: currentUser.color,
clientId: currentUser.clientId,
});
},
[flowInstance],
Expand Down
52 changes: 52 additions & 0 deletions apps/frontend/src/hooks/useSyncedUsers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { useEffect } from "react";

import usePageStore from "@/store/usePageStore";
import useUserStore, { User } from "@/store/useUserStore";

export const useSyncedUsers = () => {
const { currentPage } = usePageStore();
const { provider, currentUser, setCurrentUser, setUsers } = useUserStore();

const updateUsersFromAwareness = () => {
const values = Array.from(
provider.awareness.getStates().values(),
) as User[];
setUsers(values);
};

const getLocalStorageUser = (): User | null => {
const userData = localStorage.getItem("currentUser");
return userData ? JSON.parse(userData) : null;
};

useEffect(() => {
if (currentPage === null) return;

const updatedUser: User = {
...currentUser,
currentPageId: currentPage.toString(),
};

setCurrentUser(updatedUser);
provider.awareness.setLocalState(updatedUser);
}, [currentPage]);

useEffect(() => {
const localStorageUser = getLocalStorageUser();

if (!localStorageUser) {
localStorage.setItem("currentUser", JSON.stringify(currentUser));
} else {
setCurrentUser(localStorageUser);
provider.awareness.setLocalState(localStorageUser);
}

updateUsersFromAwareness();

provider.awareness.on("change", updateUsersFromAwareness);

return () => {
provider.awareness.off("change", updateUsersFromAwareness);
};
}, []);
};
20 changes: 20 additions & 0 deletions apps/frontend/src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,23 @@ import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

export function getRandomColor() {
const COLORS = [
"#7d7b94",
"#41c76d",
"#f86e7e",
"#f6b8b8",
"#f7d353",
"#3b5bf7",
"#59cbf7",
] as const;

return COLORS[Math.floor(Math.random() * COLORS.length)];
}

export function getRandomHexString(length = 10) {
return [...Array(length)]
.map(() => Math.floor(Math.random() * 16).toString(16))
.join("");
}
46 changes: 46 additions & 0 deletions apps/frontend/src/store/useUserStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { create } from "zustand";
import * as Y from "yjs";
import { SocketIOProvider } from "y-socket.io";

import { getRandomColor, getRandomHexString } from "@/lib/utils";

export interface User {
clientId: string;
color: string;
currentPageId: string | null;
}

interface UserStore {
provider: SocketIOProvider;
users: User[];
currentUser: User;
setUsers: (users: User[]) => void;
setCurrentUser: (user: User) => void;
}

const useUserStore = create<UserStore>((set) => ({
provider: new SocketIOProvider(
import.meta.env.VITE_WS_URL,
`users`,
new Y.Doc(),
{
autoConnect: true,
disableBc: false,
},
{
reconnectionDelayMax: 10000,
timeout: 5000,
transports: ["websocket", "polling"],
},
),
users: [],
currentUser: {
clientId: getRandomHexString(10),
color: getRandomColor(),
currentPageId: null,
},
setUsers: (users: User[]) => set({ users }),
setCurrentUser: (user: User) => set({ currentUser: user }),
}));

export default useUserStore;

0 comments on commit 2ddd6ac

Please sign in to comment.