diff --git a/packages/frontend/src/App.tsx b/packages/frontend/src/App.tsx index 7f41c5ed..cfacd7bd 100644 --- a/packages/frontend/src/App.tsx +++ b/packages/frontend/src/App.tsx @@ -3,6 +3,7 @@ import { BrowserRouter, Route, Routes } from "react-router-dom"; import "./App.css"; import Editor from "./components/note/Editor.tsx"; import Home from "./pages/Home.tsx"; +import NotFoundPage from "./pages/NotFound.tsx"; import SpacePage from "./pages/Space.tsx"; function App() { @@ -12,6 +13,7 @@ function App() { } /> } /> } /> + } /> ); diff --git a/packages/frontend/src/assets/error-logo.svg b/packages/frontend/src/assets/error-logo.svg new file mode 100644 index 00000000..5bc9c350 --- /dev/null +++ b/packages/frontend/src/assets/error-logo.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/frontend/src/components/ErrorSection.tsx b/packages/frontend/src/components/ErrorSection.tsx new file mode 100644 index 00000000..e5cf5956 --- /dev/null +++ b/packages/frontend/src/components/ErrorSection.tsx @@ -0,0 +1,29 @@ +import { ReactNode } from "react"; + +import buzzyLogo from "@/assets/error-logo.svg"; + +type ErrorSectionProps = { + description: string; + RestoreActions?: () => ReactNode; +}; + +export default function ErrorSection({ + description, + RestoreActions, +}: ErrorSectionProps) { + return ( +
+
+ +
+

{description}

+
+ {RestoreActions && ( +
+ +
+ )} +
+
+ ); +} diff --git a/packages/frontend/src/components/space/SpaceView.tsx b/packages/frontend/src/components/space/SpaceView.tsx index 91df39af..a0c23e77 100644 --- a/packages/frontend/src/components/space/SpaceView.tsx +++ b/packages/frontend/src/components/space/SpaceView.tsx @@ -296,7 +296,8 @@ export default function SpaceView({ spaceId, autofitTo }: SpaceViewProps) { }} > diff --git a/packages/frontend/src/hooks/yjs/useYjsConnection.tsx b/packages/frontend/src/hooks/yjs/useYjsConnection.tsx index 23d19172..3f13fbc9 100644 --- a/packages/frontend/src/hooks/yjs/useYjsConnection.tsx +++ b/packages/frontend/src/hooks/yjs/useYjsConnection.tsx @@ -6,10 +6,16 @@ import * as Y from "yjs"; import { generateUserColor } from "@/lib/utils"; export default function useYjsConnection(docName: string) { + const [status, setStatus] = useState< + "connecting" | "connected" | "disconnected" + >("connecting"); + const [error, setError] = useState(); const [yDoc, setYDoc] = useState(); const [yProvider, setYProvider] = useState(); useEffect(() => { + setStatus("connecting"); + const doc = new Y.Doc(); const provider = new WebsocketProvider( `ws://${import.meta.env.DEV ? "localhost" : "www.honeyflow.life"}/ws/space`, @@ -28,9 +34,17 @@ export default function useYjsConnection(docName: string) { if (event.status === "connected") { awareness.setLocalStateField("color", generateUserColor()); } + setStatus(event.status); }, ); + provider.once("connection-close", (event: CloseEvent) => { + if (event.code === 1008) { + provider.shouldConnect = false; + setError(new Error("찾을 수 없거나 접근할 수 없는 스페이스예요.")); + } + }); + return () => { if (provider.bcconnected || provider.wsconnected) { provider.disconnect(); @@ -41,5 +55,5 @@ export default function useYjsConnection(docName: string) { }; }, [docName]); - return { yProvider, yDoc, setYProvider, setYDoc }; + return { status, error, yProvider, yDoc, setYProvider, setYDoc }; } diff --git a/packages/frontend/src/lib/utils.ts b/packages/frontend/src/lib/utils.ts index 5f0d8703..bc348d84 100644 --- a/packages/frontend/src/lib/utils.ts +++ b/packages/frontend/src/lib/utils.ts @@ -88,7 +88,7 @@ export function createSafeContext(defaultValue?: T) { } export function generateUniqueId() { - return Math.random().toString(36); + return Math.random().toString(36).slice(2); } // 노출과 명도는 유지, 색상만 랜덤 diff --git a/packages/frontend/src/pages/Home.tsx b/packages/frontend/src/pages/Home.tsx index 1a9ad136..1f64b66e 100644 --- a/packages/frontend/src/pages/Home.tsx +++ b/packages/frontend/src/pages/Home.tsx @@ -31,7 +31,9 @@ type CreateSpaceButtonProps = { function CreateSpaceButton({ navigate }: CreateSpaceButtonProps) { const [spaceName, setSpaceName] = useState(""); + const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(""); + const handleCreateSpace = (e: FormEvent) => { e.preventDefault(); const targetSpaceName = spaceName.trim(); @@ -41,6 +43,8 @@ function CreateSpaceButton({ navigate }: CreateSpaceButtonProps) { return; } + setIsLoading(true); + requestCreateSpace(targetSpaceName) .then((res) => { const { urlPath } = res; @@ -48,6 +52,9 @@ function CreateSpaceButton({ navigate }: CreateSpaceButtonProps) { }) .catch((error) => { setError(`스페이스 생성에 실패했어요. (${error})`); + }) + .finally(() => { + setIsLoading(false); }); }; @@ -77,9 +84,21 @@ function CreateSpaceButton({ navigate }: CreateSpaceButtonProps) { {error &&

{error}

} - + - + diff --git a/packages/frontend/src/pages/NotFound.tsx b/packages/frontend/src/pages/NotFound.tsx new file mode 100644 index 00000000..a6d63fe6 --- /dev/null +++ b/packages/frontend/src/pages/NotFound.tsx @@ -0,0 +1,22 @@ +import { Link } from "react-router-dom"; + +import { MoveLeftIcon } from "lucide-react"; + +import ErrorSection from "@/components/ErrorSection"; +import { Button } from "@/components/ui/button"; + +export default function NotFoundPage() { + return ( + ( + + )} + /> + ); +} diff --git a/packages/frontend/src/pages/Space.tsx b/packages/frontend/src/pages/Space.tsx index 8873839f..308edb2e 100644 --- a/packages/frontend/src/pages/Space.tsx +++ b/packages/frontend/src/pages/Space.tsx @@ -1,8 +1,12 @@ import { useRef } from "react"; -import { useParams } from "react-router-dom"; +import { useNavigate, useParams } from "react-router-dom"; +import { CircleDashedIcon, MoveLeftIcon } from "lucide-react"; + +import ErrorSection from "@/components/ErrorSection"; import SpacePageHeader from "@/components/space/SpacePageHeader"; import SpaceView from "@/components/space/SpaceView"; +import { Button } from "@/components/ui/button"; import useYjsConnection from "@/hooks/yjs/useYjsConnection"; import { YjsStoreProvider } from "@/store/yjs"; @@ -11,15 +15,41 @@ interface SpacePageParams extends Record { } export default function SpacePage() { + const navigate = useNavigate(); const { spaceId } = useParams(); if (!spaceId) { throw new Error(""); } - const { yDoc, yProvider, setYDoc, setYProvider } = useYjsConnection(spaceId); + const { error, status, yDoc, yProvider, setYDoc, setYProvider } = + useYjsConnection(spaceId); const containerRef = useRef(null); + if (error) { + return ( + ( + <> + + + )} + /> + ); + } + + if (status === "connecting") { + return ( +
+ +
+ ); + } + return (