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 (
+
+
+
+
+ {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 (