From c1f1e2e22f4166087d8b5c18244fe0778e828a57 Mon Sep 17 00:00:00 2001
From: devleejb <devleejb@gmail.com>
Date: Tue, 23 Jan 2024 15:47:52 +0900
Subject: [PATCH 1/4] Generate share url

---
 ...eate-workspace-document-share-token.dto.ts |   2 -
 .../src/components/common/ShareButton.tsx     |  23 +++
 .../src/components/headers/EditorHeader.tsx   |   6 +-
 .../src/components/modals/MemberModal.tsx     |   2 +-
 frontend/src/components/modals/ShareModal.tsx | 148 ++++++++++++++++++
 frontend/src/hooks/api/types/document.d.ts    |   1 +
 .../hooks/api/types/workspaceDocument.d.ts    |   9 ++
 frontend/src/hooks/api/workspaceDocument.ts   |  15 ++
 .../src/utils/{invitation.ts => expire.ts}    |   0
 frontend/src/utils/share.ts                   |   4 +
 10 files changed, 206 insertions(+), 4 deletions(-)
 create mode 100644 frontend/src/components/common/ShareButton.tsx
 create mode 100644 frontend/src/components/modals/ShareModal.tsx
 rename frontend/src/utils/{invitation.ts => expire.ts} (100%)
 create mode 100644 frontend/src/utils/share.ts

diff --git a/backend/src/workspace-documents/dto/create-workspace-document-share-token.dto.ts b/backend/src/workspace-documents/dto/create-workspace-document-share-token.dto.ts
index dcc2a9a6..73cf60ed 100644
--- a/backend/src/workspace-documents/dto/create-workspace-document-share-token.dto.ts
+++ b/backend/src/workspace-documents/dto/create-workspace-document-share-token.dto.ts
@@ -1,6 +1,5 @@
 import { ApiProperty } from "@nestjs/swagger";
 import { Type } from "class-transformer";
-import { IsDate } from "class-validator";
 import { ShareRoleEnum } from "src/utils/constants/share-role";
 
 export class CreateWorkspaceDocumentShareTokenDto {
@@ -9,6 +8,5 @@ export class CreateWorkspaceDocumentShareTokenDto {
 
 	@ApiProperty({ type: Date, description: "Share link expiration date" })
 	@Type(() => Date)
-	@IsDate()
 	expiredAt: Date;
 }
diff --git a/frontend/src/components/common/ShareButton.tsx b/frontend/src/components/common/ShareButton.tsx
new file mode 100644
index 00000000..6b5fbd48
--- /dev/null
+++ b/frontend/src/components/common/ShareButton.tsx
@@ -0,0 +1,23 @@
+import { IconButton } from "@mui/material";
+import ShareIcon from "@mui/icons-material/Share";
+import ShareModal from "../modals/ShareModal";
+import { useState } from "react";
+
+function ShareButton() {
+	const [shareModalOpen, setShareModalOpen] = useState(false);
+
+	const handleShareModalOpen = () => {
+		setShareModalOpen((prev) => !prev);
+	};
+
+	return (
+		<>
+			<IconButton onClick={handleShareModalOpen}>
+				<ShareIcon />
+			</IconButton>
+			<ShareModal open={shareModalOpen} onClose={handleShareModalOpen} />
+		</>
+	);
+}
+
+export default ShareButton;
diff --git a/frontend/src/components/headers/EditorHeader.tsx b/frontend/src/components/headers/EditorHeader.tsx
index 4b9c24d2..1719f461 100644
--- a/frontend/src/components/headers/EditorHeader.tsx
+++ b/frontend/src/components/headers/EditorHeader.tsx
@@ -17,6 +17,7 @@ import { EditorModeType, selectEditor, setMode } from "../../store/editorSlice";
 import ThemeButton from "../common/ThemeButton";
 import { createDocumentKey } from "../../utils/document";
 import { useNavigate } from "react-router-dom";
+import ShareButton from "../common/ShareButton";
 
 function EditorHeader() {
 	const dispatch = useDispatch();
@@ -66,7 +67,10 @@ function EditorHeader() {
 							</IconButton>
 						</Tooltip>
 					</Stack>
-					<ThemeButton />
+					<Stack direction="row" alignItems="center" gap={1}>
+						<ShareButton />
+						<ThemeButton />
+					</Stack>
 				</Stack>
 			</Toolbar>
 		</AppBar>
diff --git a/frontend/src/components/modals/MemberModal.tsx b/frontend/src/components/modals/MemberModal.tsx
index 83f2dc47..2d68857c 100644
--- a/frontend/src/components/modals/MemberModal.tsx
+++ b/frontend/src/components/modals/MemberModal.tsx
@@ -22,7 +22,7 @@ import { useMemo, useState } from "react";
 import { User } from "../../hooks/api/types/user";
 import InfiniteScroll from "react-infinite-scroller";
 import { FormContainer, SelectElement } from "react-hook-form-mui";
-import { invitationExpiredStringList } from "../../utils/invitation";
+import { invitationExpiredStringList } from "../../utils/expire";
 import moment, { unitOfTime } from "moment";
 import ContentCopyIcon from "@mui/icons-material/ContentCopy";
 import clipboard from "clipboardy";
diff --git a/frontend/src/components/modals/ShareModal.tsx b/frontend/src/components/modals/ShareModal.tsx
new file mode 100644
index 00000000..67574cd8
--- /dev/null
+++ b/frontend/src/components/modals/ShareModal.tsx
@@ -0,0 +1,148 @@
+import {
+	Button,
+	FormControl,
+	IconButton,
+	Modal,
+	ModalProps,
+	Paper,
+	Stack,
+	Tooltip,
+	Typography,
+} from "@mui/material";
+import { FormContainer, SelectElement } from "react-hook-form-mui";
+import { invitationExpiredStringList } from "../../utils/expire";
+import { useState } from "react";
+import moment, { unitOfTime } from "moment";
+import { useParams } from "react-router";
+import { useGetDocumentQuery } from "../../hooks/api/document";
+import { useCreateWorkspaceSharingTokenMutation } from "../../hooks/api/workspaceDocument";
+import { ShareRole } from "../../utils/share";
+import clipboard from "clipboardy";
+import { useSnackbar } from "notistack";
+import ContentCopyIcon from "@mui/icons-material/ContentCopy";
+import CloseIcon from "@mui/icons-material/Close";
+
+interface ShareModalProps extends Omit<ModalProps, "children"> {}
+
+function ShareModal(props: ShareModalProps) {
+	const { ...modalProps } = props;
+	const params = useParams();
+	const [shareUrl, setShareUrl] = useState<string | null>(null);
+	const { data: document } = useGetDocumentQuery(params.documentSlug || "");
+	const { mutateAsync: createWorkspaceSharingToken } = useCreateWorkspaceSharingTokenMutation(
+		document?.workspaceId || "",
+		document?.id || ""
+	);
+	const { enqueueSnackbar } = useSnackbar();
+
+	const handleCreateShareUrl = async (data: { expiredString: string; role: ShareRole }) => {
+		let addedTime: Date | null;
+
+		if (data.expiredString === invitationExpiredStringList[0]) {
+			addedTime = null;
+		} else {
+			const [num, unit] = data.expiredString.split(" ");
+			addedTime = moment()
+				.add(Number(num), unit as unitOfTime.DurationConstructor)
+				.toDate();
+		}
+
+		const { sharingToken } = await createWorkspaceSharingToken({
+			role: data.role,
+			expiredAt: addedTime,
+		});
+
+		setShareUrl(
+			`${window.location.origin}/document/${params.documentSlug}?token=${sharingToken}`
+		);
+	};
+
+	const handleCopyShareUrl = async () => {
+		if (!shareUrl) return;
+
+		await clipboard.write(shareUrl);
+		enqueueSnackbar("URL Copied!", { variant: "success" });
+	};
+
+	return (
+		<Modal disableAutoFocus {...modalProps}>
+			<Paper
+				sx={{
+					position: "absolute",
+					top: "50%",
+					left: "50%",
+					transform: "translate(-50%, -50%)",
+					p: 4,
+					width: 400,
+				}}
+			>
+				<IconButton
+					sx={{
+						position: "absolute",
+						top: 28,
+						right: 28,
+					}}
+					onClick={(e) => props.onClose?.(e, "backdropClick")}
+				>
+					<CloseIcon />
+				</IconButton>
+				<Stack gap={1}>
+					<Typography variant="subtitle1">Share Link</Typography>
+					<FormControl>
+						<FormContainer
+							defaultValues={{
+								expiredString: invitationExpiredStringList[0],
+								role: Object.values(ShareRole)[0],
+							}}
+							onSuccess={handleCreateShareUrl}
+						>
+							<Stack gap={2}>
+								<SelectElement
+									label="Role"
+									name="role"
+									options={Object.values(ShareRole).map((role) => ({
+										id: role,
+										label: role,
+									}))}
+									size="small"
+									sx={{
+										width: 1,
+									}}
+									variant="filled"
+								/>
+								<SelectElement
+									label="Expired Date"
+									name="expiredString"
+									options={invitationExpiredStringList.map((expiredString) => ({
+										id: expiredString,
+										label: expiredString,
+									}))}
+									size="small"
+									sx={{
+										width: 1,
+									}}
+									variant="filled"
+								/>
+								<Button type="submit" variant="contained">
+									Generate
+								</Button>
+							</Stack>
+						</FormContainer>
+					</FormControl>
+					{Boolean(shareUrl) && (
+						<Stack direction="row" alignItems="center" gap={2}>
+							<Typography variant="body1">{shareUrl}</Typography>
+							<Tooltip title="Copy URL">
+								<IconButton onClick={handleCopyShareUrl}>
+									<ContentCopyIcon />
+								</IconButton>
+							</Tooltip>
+						</Stack>
+					)}
+				</Stack>
+			</Paper>
+		</Modal>
+	);
+}
+
+export default ShareModal;
diff --git a/frontend/src/hooks/api/types/document.d.ts b/frontend/src/hooks/api/types/document.d.ts
index d746e103..7a29116e 100644
--- a/frontend/src/hooks/api/types/document.d.ts
+++ b/frontend/src/hooks/api/types/document.d.ts
@@ -1,5 +1,6 @@
 export class Document {
 	id: string;
+	workspaceId: string;
 	yorkieDocumentId: string;
 	title: string;
 	slug: string;
diff --git a/frontend/src/hooks/api/types/workspaceDocument.d.ts b/frontend/src/hooks/api/types/workspaceDocument.d.ts
index ee35a854..05ae6db4 100644
--- a/frontend/src/hooks/api/types/workspaceDocument.d.ts
+++ b/frontend/src/hooks/api/types/workspaceDocument.d.ts
@@ -10,3 +10,12 @@ export class CreateDocumentRequest {
 }
 
 export class CreateDocumentResponse extends Document {}
+
+export class CreateDocumentShareTokenRequest {
+	role: ShareRole;
+	expiredAt: Date | null;
+}
+
+export class CreateDocumentShareTokenResponse {
+	sharingToken: string;
+}
diff --git a/frontend/src/hooks/api/workspaceDocument.ts b/frontend/src/hooks/api/workspaceDocument.ts
index bb81a02e..ad4da6fb 100644
--- a/frontend/src/hooks/api/workspaceDocument.ts
+++ b/frontend/src/hooks/api/workspaceDocument.ts
@@ -3,6 +3,8 @@ import axios from "axios";
 import {
 	CreateDocumentRequest,
 	CreateDocumentResponse,
+	CreateDocumentShareTokenRequest,
+	CreateDocumentShareTokenResponse,
 	GetWorkspaceDocumentListResponse,
 } from "./types/workspaceDocument";
 
@@ -53,3 +55,16 @@ export const useCreateDocumentMutation = (workspaceId: string) => {
 		},
 	});
 };
+
+export const useCreateWorkspaceSharingTokenMutation = (workspaceId: string, documentId: string) => {
+	return useMutation({
+		mutationFn: async (data: CreateDocumentShareTokenRequest) => {
+			const res = await axios.post<CreateDocumentShareTokenResponse>(
+				`/workspaces/${workspaceId}/documents/${documentId}/share-token`,
+				data
+			);
+
+			return res.data;
+		},
+	});
+};
diff --git a/frontend/src/utils/invitation.ts b/frontend/src/utils/expire.ts
similarity index 100%
rename from frontend/src/utils/invitation.ts
rename to frontend/src/utils/expire.ts
diff --git a/frontend/src/utils/share.ts b/frontend/src/utils/share.ts
new file mode 100644
index 00000000..27cfc72c
--- /dev/null
+++ b/frontend/src/utils/share.ts
@@ -0,0 +1,4 @@
+export enum ShareRole {
+	READ = "READ",
+	EDIT = "EDIT",
+}

From 1b20758323f6240a1be2dfb3cd120cb7c58538e1 Mon Sep 17 00:00:00 2001
From: devleejb <devleejb@gmail.com>
Date: Tue, 23 Jan 2024 16:23:18 +0900
Subject: [PATCH 2/4] Add document sharing

---
 .../src/components/common/ShareButton.tsx     |  2 +-
 frontend/src/components/editor/Preview.tsx    |  9 ++-
 .../src/components/headers/EditorHeader.tsx   | 69 +++++++++----------
 frontend/src/components/modals/ShareModal.tsx |  6 +-
 frontend/src/hooks/api/document.ts            | 31 ++++++++-
 frontend/src/hooks/api/types/document.d.ts    |  6 ++
 frontend/src/pages/document/Index.tsx         | 33 ++++++---
 frontend/src/routes.tsx                       |  2 +-
 frontend/src/store/editorSlice.ts             |  8 ++-
 9 files changed, 111 insertions(+), 55 deletions(-)

diff --git a/frontend/src/components/common/ShareButton.tsx b/frontend/src/components/common/ShareButton.tsx
index 6b5fbd48..f0972232 100644
--- a/frontend/src/components/common/ShareButton.tsx
+++ b/frontend/src/components/common/ShareButton.tsx
@@ -12,7 +12,7 @@ function ShareButton() {
 
 	return (
 		<>
-			<IconButton onClick={handleShareModalOpen}>
+			<IconButton onClick={handleShareModalOpen} color="inherit">
 				<ShareIcon />
 			</IconButton>
 			<ShareModal open={shareModalOpen} onClose={handleShareModalOpen} />
diff --git a/frontend/src/components/editor/Preview.tsx b/frontend/src/components/editor/Preview.tsx
index 8080f5a5..1003ab5b 100644
--- a/frontend/src/components/editor/Preview.tsx
+++ b/frontend/src/components/editor/Preview.tsx
@@ -2,7 +2,7 @@ import MarkdownPreview from "@uiw/react-markdown-preview";
 import { useCurrentTheme } from "../../hooks/useCurrentTheme";
 import { useSelector } from "react-redux";
 import { selectEditor } from "../../store/editorSlice";
-import { CircularProgress } from "@mui/material";
+import { CircularProgress, Stack } from "@mui/material";
 import { useEffect, useState } from "react";
 import "./editor.css";
 
@@ -26,7 +26,12 @@ function Preview() {
 		};
 	}, [editorStore.doc]);
 
-	if (!editorStore?.doc) return <CircularProgress sx={{ marginX: "auto", mt: 4 }} />;
+	if (!editorStore?.doc)
+		return (
+			<Stack direction="row" justifyContent="center">
+				<CircularProgress sx={{ mt: 2 }} />
+			</Stack>
+		);
 
 	return (
 		<MarkdownPreview
diff --git a/frontend/src/components/headers/EditorHeader.tsx b/frontend/src/components/headers/EditorHeader.tsx
index 1719f461..c8b10a9e 100644
--- a/frontend/src/components/headers/EditorHeader.tsx
+++ b/frontend/src/components/headers/EditorHeader.tsx
@@ -1,6 +1,5 @@
 import {
 	AppBar,
-	IconButton,
 	Paper,
 	Stack,
 	ToggleButton,
@@ -11,64 +10,60 @@ import {
 import EditIcon from "@mui/icons-material/Edit";
 import VerticalSplitIcon from "@mui/icons-material/VerticalSplit";
 import VisibilityIcon from "@mui/icons-material/Visibility";
-import AddIcon from "@mui/icons-material/Add";
 import { useDispatch, useSelector } from "react-redux";
 import { EditorModeType, selectEditor, setMode } from "../../store/editorSlice";
 import ThemeButton from "../common/ThemeButton";
-import { createDocumentKey } from "../../utils/document";
-import { useNavigate } from "react-router-dom";
 import ShareButton from "../common/ShareButton";
+import { useEffect } from "react";
 
 function EditorHeader() {
 	const dispatch = useDispatch();
 	const editorState = useSelector(selectEditor);
-	const navigate = useNavigate();
+
+	useEffect(() => {
+		if (editorState.shareRole === "READ") {
+			dispatch(setMode("read"));
+		}
+	}, [editorState.shareRole]);
 
 	const handleChangeMode = (newMode: EditorModeType) => {
 		dispatch(setMode(newMode));
 	};
 
-	const handleCreateNewDocument = () => {
-		navigate(`/${createDocumentKey()}`);
-	};
-
 	return (
 		<AppBar position="static" sx={{ zIndex: 100 }}>
 			<Toolbar>
 				<Stack width="100%" direction="row" justifyContent="space-between">
 					<Stack direction="row" spacing={1}>
 						<Paper>
-							<ToggleButtonGroup
-								value={editorState.mode}
-								exclusive
-								onChange={(_, newMode) => handleChangeMode(newMode)}
-								size="small"
-							>
-								<ToggleButton value="edit" aria-label="edit">
-									<Tooltip title="Edit Mode">
-										<EditIcon />
-									</Tooltip>
-								</ToggleButton>
-								<ToggleButton value="both" aria-label="both">
-									<Tooltip title="Both Mode">
-										<VerticalSplitIcon />
-									</Tooltip>
-								</ToggleButton>
-								<ToggleButton value="read" aria-label="read">
-									<Tooltip title="Read Mode">
-										<VisibilityIcon />
-									</Tooltip>
-								</ToggleButton>
-							</ToggleButtonGroup>
+							{editorState.shareRole !== "READ" && (
+								<ToggleButtonGroup
+									value={editorState.mode}
+									exclusive
+									onChange={(_, newMode) => handleChangeMode(newMode)}
+									size="small"
+								>
+									<ToggleButton value="edit" aria-label="edit">
+										<Tooltip title="Edit Mode">
+											<EditIcon />
+										</Tooltip>
+									</ToggleButton>
+									<ToggleButton value="both" aria-label="both">
+										<Tooltip title="Both Mode">
+											<VerticalSplitIcon />
+										</Tooltip>
+									</ToggleButton>
+									<ToggleButton value="read" aria-label="read">
+										<Tooltip title="Read Mode">
+											<VisibilityIcon />
+										</Tooltip>
+									</ToggleButton>
+								</ToggleButtonGroup>
+							)}
 						</Paper>
-						<Tooltip title="Create New Note">
-							<IconButton color="inherit" onClick={handleCreateNewDocument}>
-								<AddIcon />
-							</IconButton>
-						</Tooltip>
 					</Stack>
 					<Stack direction="row" alignItems="center" gap={1}>
-						<ShareButton />
+						{!editorState.shareRole && <ShareButton />}
 						<ThemeButton />
 					</Stack>
 				</Stack>
diff --git a/frontend/src/components/modals/ShareModal.tsx b/frontend/src/components/modals/ShareModal.tsx
index 67574cd8..87afda76 100644
--- a/frontend/src/components/modals/ShareModal.tsx
+++ b/frontend/src/components/modals/ShareModal.tsx
@@ -21,14 +21,18 @@ import clipboard from "clipboardy";
 import { useSnackbar } from "notistack";
 import ContentCopyIcon from "@mui/icons-material/ContentCopy";
 import CloseIcon from "@mui/icons-material/Close";
+import { useSearchParams } from "react-router-dom";
 
 interface ShareModalProps extends Omit<ModalProps, "children"> {}
 
 function ShareModal(props: ShareModalProps) {
 	const { ...modalProps } = props;
 	const params = useParams();
+	const [searchParams] = useSearchParams();
 	const [shareUrl, setShareUrl] = useState<string | null>(null);
-	const { data: document } = useGetDocumentQuery(params.documentSlug || "");
+	const { data: document } = useGetDocumentQuery(
+		searchParams.get("token") ? null : params.documentSlug
+	);
 	const { mutateAsync: createWorkspaceSharingToken } = useCreateWorkspaceSharingTokenMutation(
 		document?.workspaceId || "",
 		document?.id || ""
diff --git a/frontend/src/hooks/api/document.ts b/frontend/src/hooks/api/document.ts
index f67faa0e..ce2dd088 100644
--- a/frontend/src/hooks/api/document.ts
+++ b/frontend/src/hooks/api/document.ts
@@ -1,21 +1,46 @@
 import { useQuery } from "@tanstack/react-query";
 import axios from "axios";
-import { GetDocumentResponse } from "./types/document";
+import { GetDocumentBySharingTokenResponse, GetDocumentResponse } from "./types/document";
 
 export const generateGetDocumentQueryKey = (documentSlug: string) => {
 	return ["documents", documentSlug];
 };
 
-export const useGetDocumentQuery = (documentSlug: string) => {
+export const generateGetDocumentBySharingTokenQueryKey = (sharingToken: string) => {
+	return ["documents", "share", sharingToken];
+};
+
+export const useGetDocumentQuery = (documentSlug?: string | null) => {
 	const query = useQuery({
 		queryKey: generateGetDocumentQueryKey(documentSlug || ""),
 		enabled: Boolean(documentSlug),
 		queryFn: async () => {
+			console.log("zz", documentSlug);
 			const res = await axios.get<GetDocumentResponse>(`/documents/${documentSlug}`);
 			return res.data;
 		},
 		meta: {
-			errorMessage: "This is a non-existent or unauthorized Workspace.",
+			errorMessage: "This is a non-existent or unauthorized document.",
+		},
+	});
+
+	return query;
+};
+
+export const useGetDocumentBySharingTokenQuery = (sharingToken?: string | null) => {
+	const query = useQuery({
+		queryKey: generateGetDocumentQueryKey(sharingToken || ""),
+		enabled: Boolean(sharingToken),
+		queryFn: async () => {
+			const res = await axios.get<GetDocumentBySharingTokenResponse>("/documents/share", {
+				params: {
+					token: sharingToken,
+				},
+			});
+			return res.data;
+		},
+		meta: {
+			errorMessage: "This is a non-existent or expired document.",
 		},
 	});
 
diff --git a/frontend/src/hooks/api/types/document.d.ts b/frontend/src/hooks/api/types/document.d.ts
index 7a29116e..e7a594ba 100644
--- a/frontend/src/hooks/api/types/document.d.ts
+++ b/frontend/src/hooks/api/types/document.d.ts
@@ -1,3 +1,5 @@
+import { ShareRole } from "../../../utils/share";
+
 export class Document {
 	id: string;
 	workspaceId: string;
@@ -10,3 +12,7 @@ export class Document {
 }
 
 export class GetDocumentResponse extends Document {}
+
+export class GetDocumentBySharingTokenResponse extends Document {
+	role: ShareRole;
+}
diff --git a/frontend/src/pages/document/Index.tsx b/frontend/src/pages/document/Index.tsx
index 4dbd7cb2..804d7160 100644
--- a/frontend/src/pages/document/Index.tsx
+++ b/frontend/src/pages/document/Index.tsx
@@ -1,7 +1,7 @@
-import { useEffect } from "react";
+import { useContext, useEffect } from "react";
 import Editor from "../../components/editor/Editor";
 import * as yorkie from "yorkie-js-sdk";
-import { selectEditor, setClient, setDoc } from "../../store/editorSlice";
+import { selectEditor, setClient, setDoc, setShareRole } from "../../store/editorSlice";
 import { useDispatch, useSelector } from "react-redux";
 import {
 	YorkieCodeMirrorDocType,
@@ -13,21 +13,26 @@ import { Box, Paper } from "@mui/material";
 import Resizable from "react-resizable-layout";
 import { useWindowWidth } from "@react-hook/window-size";
 import Preview from "../../components/editor/Preview";
-import { useParams } from "react-router-dom";
-import { useGetDocumentQuery } from "../../hooks/api/document";
+import { useParams, useSearchParams } from "react-router-dom";
+import { useGetDocumentBySharingTokenQuery, useGetDocumentQuery } from "../../hooks/api/document";
+import { AuthContext } from "../../contexts/AuthContext";
 
 function EditorIndex() {
-	const params = useParams();
 	const dispatch = useDispatch();
+	const params = useParams();
+	const { isLoggedIn } = useContext(AuthContext);
+	const [searchParams] = useSearchParams();
 	const windowWidth = useWindowWidth();
 	const editorStore = useSelector(selectEditor);
-	const { data: document } = useGetDocumentQuery(params.documentSlug || "");
+	const { data: document } = useGetDocumentQuery(isLoggedIn ? params.documentSlug : null);
+	const { data: sharedDocument } = useGetDocumentBySharingTokenQuery(searchParams.get("token"));
 
 	useEffect(() => {
 		let client: yorkie.Client;
 		let doc: yorkie.Document<YorkieCodeMirrorDocType, YorkieCodeMirrorPresenceType>;
+		const yorkieDocuentId = document?.yorkieDocumentId || sharedDocument?.yorkieDocumentId;
 
-		if (!document?.yorkieDocumentId) return;
+		if (!yorkieDocuentId) return;
 
 		const initializeYorkie = async () => {
 			client = new yorkie.Client(import.meta.env.VITE_YORKIE_API_ADDR, {
@@ -35,7 +40,7 @@ function EditorIndex() {
 			});
 			await client.activate();
 
-			doc = new yorkie.Document(document?.yorkieDocumentId as string);
+			doc = new yorkie.Document(yorkieDocuentId as string);
 
 			await client.attach(doc, {
 				initialPresence: {
@@ -58,7 +63,17 @@ function EditorIndex() {
 
 			cleanUp();
 		};
-	}, [dispatch, document?.yorkieDocumentId]);
+	}, [dispatch, document?.yorkieDocumentId, sharedDocument?.yorkieDocumentId]);
+
+	useEffect(() => {
+		if (!sharedDocument) return;
+
+		dispatch(setShareRole(sharedDocument.role));
+
+		return () => {
+			setShareRole(null);
+		};
+	}, [dispatch, sharedDocument, sharedDocument?.role]);
 
 	return (
 		<Box height="calc(100% - 64px)">
diff --git a/frontend/src/routes.tsx b/frontend/src/routes.tsx
index de28305a..310f5c99 100644
--- a/frontend/src/routes.tsx
+++ b/frontend/src/routes.tsx
@@ -52,7 +52,7 @@ const codePairRoutes: Array<CodePairRoute> = [
 	},
 	{
 		path: "document",
-		accessType: AccessType.PRIVATE,
+		accessType: AccessType.PUBLIC,
 		element: <EditorLayout />,
 		children: [
 			{
diff --git a/frontend/src/store/editorSlice.ts b/frontend/src/store/editorSlice.ts
index 287be431..659120d1 100644
--- a/frontend/src/store/editorSlice.ts
+++ b/frontend/src/store/editorSlice.ts
@@ -3,6 +3,7 @@ import type { PayloadAction } from "@reduxjs/toolkit";
 import { RootState } from "./store";
 import * as yorkie from "yorkie-js-sdk";
 import { YorkieCodeMirrorDocType, YorkieCodeMirrorPresenceType } from "../utils/yorkie/yorkieSync";
+import { ShareRole } from "../utils/share";
 
 export type EditorModeType = "edit" | "both" | "read";
 export type CodePairDocType = yorkie.Document<
@@ -12,12 +13,14 @@ export type CodePairDocType = yorkie.Document<
 
 export interface EditorState {
 	mode: EditorModeType;
+	shareRole: ShareRole | null;
 	doc: CodePairDocType | null;
 	client: yorkie.Client | null;
 }
 
 const initialState: EditorState = {
 	mode: "both",
+	shareRole: null,
 	doc: null,
 	client: null,
 };
@@ -29,6 +32,9 @@ export const editorSlice = createSlice({
 		setMode: (state, action: PayloadAction<EditorModeType>) => {
 			state.mode = action.payload;
 		},
+		setShareRole: (state, action: PayloadAction<ShareRole | null>) => {
+			state.shareRole = action.payload;
+		},
 		setDoc: (state, action: PayloadAction<CodePairDocType | null>) => {
 			state.doc = action.payload;
 		},
@@ -38,7 +44,7 @@ export const editorSlice = createSlice({
 	},
 });
 
-export const { setMode, setDoc, setClient } = editorSlice.actions;
+export const { setMode, setDoc, setClient, setShareRole } = editorSlice.actions;
 
 export const selectEditor = (state: RootState) => state.editor;
 

From 872a02f3339ec4e5a74534f808c7fce8cae9803b Mon Sep 17 00:00:00 2001
From: devleejb <devleejb@gmail.com>
Date: Tue, 23 Jan 2024 16:26:00 +0900
Subject: [PATCH 3/4] Add permission checking to note page

---
 frontend/src/hooks/api/document.ts    |  1 -
 frontend/src/pages/document/Index.tsx | 12 +++++++++---
 2 files changed, 9 insertions(+), 4 deletions(-)

diff --git a/frontend/src/hooks/api/document.ts b/frontend/src/hooks/api/document.ts
index ce2dd088..1a3512d4 100644
--- a/frontend/src/hooks/api/document.ts
+++ b/frontend/src/hooks/api/document.ts
@@ -15,7 +15,6 @@ export const useGetDocumentQuery = (documentSlug?: string | null) => {
 		queryKey: generateGetDocumentQueryKey(documentSlug || ""),
 		enabled: Boolean(documentSlug),
 		queryFn: async () => {
-			console.log("zz", documentSlug);
 			const res = await axios.get<GetDocumentResponse>(`/documents/${documentSlug}`);
 			return res.data;
 		},
diff --git a/frontend/src/pages/document/Index.tsx b/frontend/src/pages/document/Index.tsx
index 804d7160..517043a2 100644
--- a/frontend/src/pages/document/Index.tsx
+++ b/frontend/src/pages/document/Index.tsx
@@ -13,7 +13,7 @@ import { Box, Paper } from "@mui/material";
 import Resizable from "react-resizable-layout";
 import { useWindowWidth } from "@react-hook/window-size";
 import Preview from "../../components/editor/Preview";
-import { useParams, useSearchParams } from "react-router-dom";
+import { Navigate, useParams, useSearchParams } from "react-router-dom";
 import { useGetDocumentBySharingTokenQuery, useGetDocumentQuery } from "../../hooks/api/document";
 import { AuthContext } from "../../contexts/AuthContext";
 
@@ -24,8 +24,11 @@ function EditorIndex() {
 	const [searchParams] = useSearchParams();
 	const windowWidth = useWindowWidth();
 	const editorStore = useSelector(selectEditor);
-	const { data: document } = useGetDocumentQuery(isLoggedIn ? params.documentSlug : null);
-	const { data: sharedDocument } = useGetDocumentBySharingTokenQuery(searchParams.get("token"));
+	const { data: document, isError: isDocumentError } = useGetDocumentQuery(
+		isLoggedIn ? params.documentSlug : null
+	);
+	const { data: sharedDocument, isError: isSharedDocumentError } =
+		useGetDocumentBySharingTokenQuery(searchParams.get("token"));
 
 	useEffect(() => {
 		let client: yorkie.Client;
@@ -75,6 +78,9 @@ function EditorIndex() {
 		};
 	}, [dispatch, sharedDocument, sharedDocument?.role]);
 
+	if (isDocumentError || isSharedDocumentError)
+		return <Navigate to="/" state={{ from: location }} replace />;
+
 	return (
 		<Box height="calc(100% - 64px)">
 			{/* For Markdown Preview Theme */}

From 845f6d71029314cfbc81f20df2dc23b2164be25b Mon Sep 17 00:00:00 2001
From: devleejb <devleejb@gmail.com>
Date: Tue, 23 Jan 2024 16:33:06 +0900
Subject: [PATCH 4/4] Fix lint

---
 frontend/src/components/headers/EditorHeader.tsx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/frontend/src/components/headers/EditorHeader.tsx b/frontend/src/components/headers/EditorHeader.tsx
index c8b10a9e..cf73698f 100644
--- a/frontend/src/components/headers/EditorHeader.tsx
+++ b/frontend/src/components/headers/EditorHeader.tsx
@@ -24,7 +24,7 @@ function EditorHeader() {
 		if (editorState.shareRole === "READ") {
 			dispatch(setMode("read"));
 		}
-	}, [editorState.shareRole]);
+	}, [dispatch, editorState.shareRole]);
 
 	const handleChangeMode = (newMode: EditorModeType) => {
 		dispatch(setMode(newMode));