diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 08111cd6..3d96bcd1 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -14,6 +14,8 @@ "@fortawesome/free-regular-svg-icons": "6.5.1", "@fortawesome/free-solid-svg-icons": "6.5.1", "@fortawesome/react-fontawesome": "0.2.0", + "chart.js": "^4.4.3", + "chartjs-plugin-datalabels": "^2.2.0", "http-proxy-middleware": "^2.0.6", "jquery": "^3.7.1", "prosemirror-example-setup": "^1.2.2", @@ -25,6 +27,7 @@ "prosemirror-utils": "^1.2.1-0", "prosemirror-view": "^1.33.1", "react": "17.0.2", + "react-chartjs-2": "^5.2.0", "react-dom": "17.0.2", "react-router-dom": "6.21.3", "react-scripts": "5.0.1", @@ -3346,6 +3349,11 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@kurkle/color": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.2.tgz", + "integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==" + }, "node_modules/@leichtgewicht/ip-codec": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz", @@ -5721,6 +5729,25 @@ "node": ">=10" } }, + "node_modules/chart.js": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.3.tgz", + "integrity": "sha512-qK1gkGSRYcJzqrrzdR6a+I0vQ4/R+SoODXyAjscQ/4mzuNzySaMCd+hyVxitSY1+L2fjPD1Gbn+ibNqRmwQeLw==", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, + "node_modules/chartjs-plugin-datalabels": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/chartjs-plugin-datalabels/-/chartjs-plugin-datalabels-2.2.0.tgz", + "integrity": "sha512-14ZU30lH7n89oq+A4bWaJPnAG8a7ZTk7dKf48YAzMvJjQtjrgg5Dpk9f+LbjCF6bpx3RAGTeL13IXpKQYyRvlw==", + "peerDependencies": { + "chart.js": ">=3.0.0" + } + }, "node_modules/check-types": { "version": "11.2.3", "resolved": "https://registry.npmjs.org/check-types/-/check-types-11.2.3.tgz", @@ -14827,6 +14854,15 @@ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" }, + "node_modules/react-chartjs-2": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.2.0.tgz", + "integrity": "sha512-98iN5aguJyVSxp5U3CblRLH67J8gkfyGNbiK3c+l1QI/G4irHMPQw44aEPmjVag+YKTyQ260NcF82GTQ3bdscA==", + "peerDependencies": { + "chart.js": "^4.1.1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-dev-utils": { "version": "12.0.1", "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 38ec32bf..b6c6f006 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,6 +9,8 @@ "@fortawesome/free-regular-svg-icons": "6.5.1", "@fortawesome/free-solid-svg-icons": "6.5.1", "@fortawesome/react-fontawesome": "0.2.0", + "chart.js": "^4.4.3", + "chartjs-plugin-datalabels": "^2.2.0", "http-proxy-middleware": "^2.0.6", "jquery": "^3.7.1", "prosemirror-example-setup": "^1.2.2", @@ -20,6 +22,7 @@ "prosemirror-utils": "^1.2.1-0", "prosemirror-view": "^1.33.1", "react": "17.0.2", + "react-chartjs-2": "^5.2.0", "react-dom": "17.0.2", "react-router-dom": "6.21.3", "react-scripts": "5.0.1", diff --git a/frontend/src/App.js b/frontend/src/App.js index 564af9eb..81934b38 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -6,6 +6,7 @@ import SignupPage from "./Component/Auth/SignupPage"; import NotePage from "./Component/Note/NotePage"; import UserProfileEdit from "./Component/Auth/UserProfileEdit"; import Page from "./Component/Page/Page"; +import Contribution from "./Component/Contribution/ContributionPage"; import EmailTokenHandler from "./Component/Utils/EmailTokenHandler"; import { BrowserRouter, Routes, Route } from "react-router-dom"; @@ -15,16 +16,14 @@ export default function App() { } /> - } /> } /> } /> + } /> } /> - } /> - } /> } /> } /> - } /> - } /> + } /> + } /> diff --git a/frontend/src/Component/Page/Page.js b/frontend/src/Component/Page/Page.js index a687bbc0..ce7c4c55 100644 --- a/frontend/src/Component/Page/Page.js +++ b/frontend/src/Component/Page/Page.js @@ -52,6 +52,7 @@ function Page() { const editorRef = useRef(null); const ydocRef = useRef(new Y.Doc()); const ydocProviderRef = useRef(null); + const yDocInitialized = useRef(null); const blockLikeRef = useRef(null); const blockLockRef = useRef(null); @@ -270,6 +271,7 @@ function Page() { if (!ydocRef.current) return; ydocRef.current = new Y.Doc(); + yDocInitialized.current = false; ydocProviderRef.current = new WebsocketProvider( // "wss://demos.yjs.dev/ws", // yjs 데모 서버 주소 // "ws://localhost:4000", @@ -297,12 +299,13 @@ function Page() { } else { if (isSynced) { handleUserConnection(); - ydocProviderRef.current.connect(); - } + ydocProviderRef.current.connect(); + } } // setisloaded(true); // 딜레이 없음 setTimeout(() => { setisloaded(true); + yDocInitialized.current = true; }, 300); // 딜레이 있음 }); @@ -575,7 +578,7 @@ function Page() { yUndoPlugin(), hoverButtonPlugin(blockLikeRef, blockLockRef), inlinePlaceholderPlugin(), - generateBlockIdPlugin(), + generateBlockIdPlugin({ yDocInitialized }), imagePlugin({ ...imageSettings, resizeCallback: (el, updateCallback) => { diff --git a/frontend/src/Component/Page/utils/editor/plugin/generateBlockIdPlugin.js b/frontend/src/Component/Page/utils/editor/plugin/generateBlockIdPlugin.js index d91680f7..0cc0cb1c 100644 --- a/frontend/src/Component/Page/utils/editor/plugin/generateBlockIdPlugin.js +++ b/frontend/src/Component/Page/utils/editor/plugin/generateBlockIdPlugin.js @@ -1,14 +1,19 @@ import { Plugin } from "prosemirror-state"; import { v4 as uuidv4 } from "uuid"; -export const generateBlockIdPlugin = (guidGenerator = uuidv4) => { - return new Plugin({ +export const generateBlockIdPlugin = ({ yDocInitialized, guidGenerator = uuidv4 }) => { + return new Plugin({ appendTransaction: (transactions, prevState, nextState) => { + // Yjs 문서가 초기화되지 않은 경우 동작하지 않도록 함 + if (!yDocInitialized.current) { + return null; + } + const tr = nextState.tr; let modified = false; const generatedIds = new Set(); const userId = localStorage.getItem('userId'); - + if (transactions.some(transaction => transaction.docChanged)) { const { paragraph, image } = nextState.schema.nodes; let prevNode = null; // 이전 노드를 추적하기 위한 변수 diff --git a/frontend/src/Component/Page/utils/editor/plugin/hoverButtonPlugin.js b/frontend/src/Component/Page/utils/editor/plugin/hoverButtonPlugin.js index f0ef035d..1f0a35ee 100644 --- a/frontend/src/Component/Page/utils/editor/plugin/hoverButtonPlugin.js +++ b/frontend/src/Component/Page/utils/editor/plugin/hoverButtonPlugin.js @@ -74,10 +74,6 @@ export function hoverButtonPlugin(blockLikeRef, blockLockRef) { toastr.warning("내용이 없는 블록입니다."); } else { await blockLikeRef.current.toggleLike(guid, liker, writer); - if (liker !== writer) { - this.classList.toggle("hoverButton_like"); - this.classList.toggle("hoverButton_like_fullRedHeart"); - } } } else { console.log('No UUID found for this node.'); @@ -133,10 +129,10 @@ export function hoverButtonPlugin(blockLikeRef, blockLockRef) { // 새 노드 삽입 const newNode = state.schema.nodes.paragraph.create(); - tr = state.doc.content.size === $clickPos.end($clickPos.depth) ? tr.insert(insertPos - 1, newNode) : tr.insert(insertPos, newNode); + tr = state.doc.content.size === $clickPos.end($clickPos.depth) && !isImageNode ? tr.insert(insertPos - 1, newNode) : tr.insert(insertPos, newNode); // 삽입된 노드 내부에 커서 위치시키기 - const newPos = state.doc.content.size === $clickPos.end($clickPos.depth) ? insertPos : insertPos + 1; // 노드 삽입 후 새로운 위치 조정 + const newPos = state.doc.content.size === $clickPos.end($clickPos.depth) && !isImageNode ? insertPos : insertPos + 1; // 노드 삽입 후 새로운 위치 조정 tr = tr.setSelection(Selection.near(tr.doc.resolve(newPos))); // 트랜잭션 적용 diff --git a/frontend/src/Component/Page/utils/yjs/BlockLike.js b/frontend/src/Component/Page/utils/yjs/BlockLike.js index 3d0aa2de..71a08687 100644 --- a/frontend/src/Component/Page/utils/yjs/BlockLike.js +++ b/frontend/src/Component/Page/utils/yjs/BlockLike.js @@ -8,10 +8,11 @@ const BlockLike = forwardRef(({ ydocRef }, ref) => { const pathSegments = location.pathname.split('/').filter(Boolean); const organizationId = pathSegments[1]; const noteId = pathSegments[2]; + const hoverButton_like = document.querySelector(".hoverButton_like"); const userId = localStorage.getItem('userId'); const yLikeList = ydocRef.current.getMap(`yLikeList_${userId}`); - + const toggleLike = async (blockId, lover, heartReceiver) => { if (lover !== heartReceiver) { const currentLikeState = yLikeList.get(blockId); @@ -34,8 +35,10 @@ const BlockLike = forwardRef(({ ydocRef }, ref) => { if (response.ok) { if (responseData.includes("좋아요 성공!")) { toastr.success(responseData); + hoverButton_like.classList.replace('hoverButton_like', 'hoverButton_like_fullRedHeart'); } else { toastr.info(responseData); + hoverButton_like.classList.replace('hoverButton_like_fullRedHeart', 'hoverButton_like'); } } else { toastr.error(responseData); diff --git a/frontend/src/Component/Page/utils/yjs/BlockLock.js b/frontend/src/Component/Page/utils/yjs/BlockLock.js index a3a0cb37..387c549b 100644 --- a/frontend/src/Component/Page/utils/yjs/BlockLock.js +++ b/frontend/src/Component/Page/utils/yjs/BlockLock.js @@ -18,13 +18,14 @@ const BlockLock = forwardRef(({ ydocRef, editorRef }, ref) => { const yRequestUnLock = ydocRef.current.getMap('yRequestUnLock'); const yConnectedUserList = ydocRef.current.getMap('connectedUsers'); const yUnLockInfo = ydocRef.current.getMap('yUnLockInfo'); + const yRecentUnLockBlock = ydocRef.current.getArray('yRecentUnLockBlock'); const yResultUnLock = ydocRef.current.getMap('yResultUnLock'); const yReceivedMessage = ydocRef.current.getMap(`${nickname}_message`); const baseSwal = Swal.mixin({ showCancelButton: true, - confirmButtonColor: "#3085d6", - cancelButtonColor: "#d33", + confirmButtonColor: "#28a745", + cancelButtonColor: "#6c757d", confirmButtonText: '확인', cancelButtonText: '취소' }); @@ -42,10 +43,14 @@ const BlockLock = forwardRef(({ ydocRef, editorRef }, ref) => { } }; + function updateHoverDivPosition(hoverDiv, change) { + const hoverDivcurrentTop = parseFloat(window.getComputedStyle(hoverDiv)?.top || "0"); + const newTop = window.matchMedia("(max-width: 768px)").matches ? hoverDivcurrentTop + change : hoverDivcurrentTop + (change * 2); + hoverDiv.style.top = `${newTop}px`; + } + function addIdToParagraph(uuid) { const hoverDiv = document.querySelector(".hoverDiv"); - const hoverDivcurrentTop = parseFloat(window.getComputedStyle(hoverDiv).top); - const view = editorRef.current.view; const { state, dispatch } = view; const { tr } = state; @@ -62,15 +67,15 @@ const BlockLock = forwardRef(({ ydocRef, editorRef }, ref) => { if (paragraphNode) { const { node, pos } = paragraphNode; const paragraphWithId = node.type.create({ ...node.attrs, id: 'locked' }, node.content, node.marks); - dispatch(tr.replaceWith(pos, pos + node.nodeSize, paragraphWithId)); - view.updateState(state.apply(tr)); - hoverDiv.style.top = window.matchMedia("(max-width: 768px)").matches ? `${hoverDivcurrentTop + 2}px` : `${hoverDivcurrentTop + 4}px`; + tr.replaceWith(pos, pos + node.nodeSize, paragraphWithId); + dispatch(tr); + updateHoverDivPosition(hoverDiv, 2); } } function removeIdFromParagraph(uuid) { const hoverDiv = document.querySelector(".hoverDiv"); - const hoverDivcurrentTop = parseFloat(window.getComputedStyle(hoverDiv).top); const view = editorRef.current.view; + const view = editorRef.current.view; const { state, dispatch } = view; const { tr } = state; @@ -88,12 +93,12 @@ const BlockLock = forwardRef(({ ydocRef, editorRef }, ref) => { if (!node.isText && !node.isInline) { const { id, ...attrsWithoutId } = node.attrs; const newAttrs = { ...attrsWithoutId, id: "non-locked" }; - dispatch(tr.setNodeMarkup(pos, null, newAttrs)); - view.updateState(state.apply(tr)); - hoverDiv.style.top = window.matchMedia("(max-width: 768px)").matches ? `${hoverDivcurrentTop - 2}px` : `${hoverDivcurrentTop - 4}px`; + tr.setNodeMarkup(pos, null, newAttrs); + dispatch(tr); + updateHoverDivPosition(hoverDiv, -2); } } - } + } // 특정 UUID로 노드를 찾아 해당 노드의 위치로 커서를 이동시키는 함수 function moveCursorToNodeWithUUID(uuid) { @@ -122,7 +127,10 @@ const BlockLock = forwardRef(({ ydocRef, editorRef }, ref) => { const selfUnlockBlock = (locker, myLockedBlockId) => { const unlockRequestor = yRequestUnLock.get(locker)?.requestor; - if (yResultUnLock.get(`${unlockRequestor}`)?.result === "deny") yResultUnLock.set(`${unlockRequestor}`, { responser: locker, result: "lateAccept", unlockedBlockID: myLockedBlockId }); + if (yResultUnLock.get(`${unlockRequestor}`)?.result === "deny") { + yResultUnLock.set(`${unlockRequestor}`, { responser: locker, result: "lateAccept", unlockedBlockID: myLockedBlockId }); + yRecentUnLockBlock.push([`${myLockedBlockId}`]); + } removeYjsMapUnLockData(nickname); removeIdFromParagraph(myLockedBlockId); yLineLocks.delete(myLockedBlockId); @@ -131,9 +139,12 @@ const BlockLock = forwardRef(({ ydocRef, editorRef }, ref) => { const removeYjsMapUnLockData = (locker) => { const unlockRequestor = yRequestUnLock.get(locker)?.requestor; + const unlockedBlockID = yResultUnLock.get(unlockRequestor)?.unlockedBlockID; + const yRecentUnLockBlockIndex = yRecentUnLockBlock.toArray().indexOf(unlockedBlockID?.toString()) if (yResultUnLock.has(`${unlockRequestor}`)) yResultUnLock.delete(`${unlockRequestor}`); if (yRequestUnLock.has(locker)) yRequestUnLock.delete(locker); if (yUnLockInfo.has(locker)) yUnLockInfo.delete(locker); + if (yRecentUnLockBlockIndex !== -1) yRecentUnLockBlock.delete(`${yRecentUnLockBlockIndex}`, 1); }; const checkRequestUnLockTimer = async (locker) => { @@ -146,7 +157,7 @@ const BlockLock = forwardRef(({ ydocRef, editorRef }, ref) => { let timerInterval; // 시간 차이를 밀리초 단위로 계산 (1분 = 60,000밀리초) if (timeDifference < expirationTime) { - const result = await baseSwal.fire({ html: `${locker} 이(가) 해당 블록의 잠금 해제 요청을 거절했습니다.`, + const result = await baseSwal.fire({ html: `${locker} 이(가) 해당 블록의 잠금 해제 요청을 거절했습니다.`, footer: `추가적인 요청은 초 후에 가능합니다.`, icon: "error", timer: expirationTime - timeDifference, @@ -184,8 +195,8 @@ const BlockLock = forwardRef(({ ydocRef, editorRef }, ref) => { if (unlockRequestor && myLockedBlockId && unlockRequestor !== nickname) { let timerInterval; let forcedModalClose = false; - const expirationTime = yRequestUnLock.get(nickname)?.expirationTime * 1000; - const result = await baseSwal.fire({ html: `${unlockRequestor} 이(가) 블록 잠금 해제를 요청하였습니다. + const expirationTime = 30000; // 요청 만료 시간(30초) + const result = await baseSwal.fire({ html: `${unlockRequestor} 이(가) 블록 잠금 해제를 요청하였습니다.
최근 설정한 블록 잠금을 해제하시겠습니까?

@@ -244,6 +255,7 @@ const BlockLock = forwardRef(({ ydocRef, editorRef }, ref) => { yUserLocks.delete(nickname); removeIdFromParagraph(myLockedBlockId); toastr.info(`편집 잠금이 해제되었습니다.`); + yRecentUnLockBlock.push([`${myLockedBlockId}`]); yResultUnLock.set(`${unlockRequestor}`, { responser: nickname, result: "accept", unlockedBlockID: myLockedBlockId }); } else if (forcedModalClose === false) { const yReceivedMessage = ydocRef.current.getMap(`${unlockRequestor}_message`); @@ -300,77 +312,88 @@ const BlockLock = forwardRef(({ ydocRef, editorRef }, ref) => { }; const toggleLineLock = async (guid) => { - const locker = yLineLocks.get(guid.toString()); - const myLockedBlockId = yUserLocks.get(nickname); + try { + const locker = yLineLocks.get(guid.toString()); + const myLockedBlockId = yUserLocks.get(nickname); - isLoggedIn(); - toastr.remove(); + isLoggedIn(); + toastr.remove(); - if (locker && locker !== nickname && myLockedBlockId && myLockedBlockId !== guid.toString()) { - const result = await baseSwal.fire({ - title: "🔓", - html: `기존에 설정한 잠금을 해제 후 요청을 보내시겠습니까?`, - }); - - if (result.isConfirmed) { - removeIdFromParagraph(myLockedBlockId); - yLineLocks.delete(myLockedBlockId); - yUserLocks.delete(nickname); - } else { - return; + if (yRecentUnLockBlock.toArray().indexOf(guid.toString()) !== -1) { toastr.warning(`잠시 후 시도하세요.
사유: 블록 잠금 정보가 남아있음`); return; } + + if (locker && locker !== nickname && myLockedBlockId && myLockedBlockId !== guid.toString()) { + const result = await baseSwal.fire({ + title: "🔓", + html: `기존에 설정한 잠금을 해제 후 요청을 보내시겠습니까?`, + }); + + if (result.isConfirmed) { + removeIdFromParagraph(myLockedBlockId); + yLineLocks.delete(myLockedBlockId); + yUserLocks.delete(nickname); + } else { + return; + } } - } - if (!locker && myLockedBlockId && myLockedBlockId !== guid.toString()) { - const result = await baseSwal.fire({ - title: "최대 1개의 블록 잠금이 허용됩니다.", - text: "이전에 설정한 잠금을 해제하시겠습니까?", - icon: "warning", - }); + if (!locker && myLockedBlockId && myLockedBlockId !== guid.toString()) { + const result = await baseSwal.fire({ + title: "최대 1개의 블록 잠금이 허용됩니다.", + text: "이전에 설정한 잠금을 해제하시겠습니까?", + icon: "warning", + }); + + if (result.isConfirmed) { + selfUnlockBlock(nickname, myLockedBlockId) + } else { + return; + } + } - if (result.isConfirmed) { - selfUnlockBlock(nickname, myLockedBlockId) + if (locker) { + if (locker === nickname) { + selfUnlockBlock(locker, myLockedBlockId) + toastr.info(`편집 잠금이 해제되었습니다.`); + return; + } + if (yRequestUnLock.has(locker) && !yUnLockInfo.has(locker)) { + const requestor = yRequestUnLock.get(locker)?.requestor + const message = requestor === nickname ? `이전 요청을 처리 중입니다...` : `${requestor} 이(가) 잠금 해제 요청 중입니다.`; + toastr.warning(message); + } else { + const beforeRequest = await checkRequestUnLockTimer(locker); + if(beforeRequest) { + const result = await baseSwal.fire({ + title: "✉️", + html: `${locker} 에게 블록 잠금 해제를 요청합니다.`, + }); + if (result.isConfirmed) { + if (!yUserLocks.has(locker) || guid?.toString() !== yUserLocks.get(locker)?.toString()) { + toastr.remove(); + toastr.warning(`다시 시도하세요.
사유: 블록 잠금 정보가 변경됨`); + } else if (yRequestUnLock.has(locker)) { + const requestor = yRequestUnLock.get(locker)?.requestor; + toastr.remove(); + toastr.warning(`${requestor} 이(가) 이미 요청했습니다.`); + } else { + yRequestUnLock.set(locker, { requestor: nickname }); + } + } + } + } } else { - return; + removeYjsMapUnLockData(nickname); + yLineLocks.set(guid.toString(), nickname); + yUserLocks.set(nickname, guid.toString()); + addIdToParagraph(guid.toString()); + toastr.success(`블록 편집 잠금이 설정되었습니다.`); } - } - - if (locker) { - if (locker === nickname) { - selfUnlockBlock(locker, myLockedBlockId) - toastr.info(`편집 잠금이 해제되었습니다.`); - return; - } - if (yRequestUnLock.has(locker) && !yUnLockInfo.has(locker)) { - const requestor = yRequestUnLock.get(locker)?.requestor - const message = requestor === nickname ? `이전 요청을 처리 중입니다...` : `${requestor} 이(가) 잠금 해제 요청 중입니다.`; - toastr.warning(message); - } else { - const beforeRequest = await checkRequestUnLockTimer(locker); - if(beforeRequest) { - const result = await baseSwal.fire({ - title: "✉️", - html: `${locker} 에게 블록 잠금 해제를 요청합니다. -
- 요청 만료 시간(초)을 설정해주세요.`, - input: "range", - inputAttributes: { - min: "15", - max: "30", - step: "5" - }, - inputValue: 15 - }); - if (result.isConfirmed) { - yRequestUnLock.set(locker, { requestor: nickname, expirationTime: result.value }); - } - } - } - } else { - yLineLocks.set(guid.toString(), nickname); - yUserLocks.set(nickname, guid.toString()); - addIdToParagraph(guid.toString()); - toastr.success(`블록 편집 잠금이 설정되었습니다.`); + } catch (error) { + const hoverDiv = document.querySelector(".hoverDiv"); + hoverDiv.style.visibility = "hidden"; + toastr.remove(); + toastr.warning(`다시 시도하세요.`); + console.error(error); } }; @@ -395,7 +418,6 @@ const BlockLock = forwardRef(({ ydocRef, editorRef }, ref) => { } }); }; - removeYjsMapUnLockData(nickname); window.addEventListener('popstate', handlePopState); yRequestUnLock.observe(checkRequestUnLockWrapper); yResultUnLock.observe(checkResultUnLockWrapper);