From 0dc0e39e3d16083d78e72ecf34e226ce79ba6c68 Mon Sep 17 00:00:00 2001 From: pkh0106 Date: Wed, 8 Jan 2025 13:43:48 +0900 Subject: [PATCH 01/26] =?UTF-8?q?refactor:=20=EC=97=90=EB=94=94=ED=84=B0?= =?UTF-8?q?=20unsaved=20=EB=B1=83=EC=A7=80=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 미사용 기능이므로 제거 --- .../src/features/editor/ui/Editor/index.tsx | 9 +-------- .../features/editor/ui/EditorActionPanel/index.tsx | 8 +------- .../src/features/editor/ui/SaveStatus/index.tsx | 11 ----------- .../src/widgets/EditorView/model/useEditorView.ts | 14 +------------- apps/frontend/src/widgets/EditorView/ui/index.tsx | 14 +++----------- 5 files changed, 6 insertions(+), 50 deletions(-) delete mode 100644 apps/frontend/src/features/editor/ui/SaveStatus/index.tsx diff --git a/apps/frontend/src/features/editor/ui/Editor/index.tsx b/apps/frontend/src/features/editor/ui/Editor/index.tsx index 403a0dee..3bc832d2 100644 --- a/apps/frontend/src/features/editor/ui/Editor/index.tsx +++ b/apps/frontend/src/features/editor/ui/Editor/index.tsx @@ -7,7 +7,6 @@ import { type JSONContent, EditorCommandList, EditorBubble, - type EditorInstance, } from "novel"; import { ImageResizer, handleCommandNavigation } from "novel/extensions"; import Collaboration from "@tiptap/extension-collaboration"; @@ -28,19 +27,14 @@ import { ColorSelector } from "./selectors/color-selector"; import { uploadFn } from "../../model/upload"; import { useEditor } from "../../model/useEditor"; -type EditorUpdateEvent = { - editor: EditorInstance; -}; - interface EditorProp { pageId: number; initialContent?: JSONContent; - onEditorUpdate?: (event: EditorUpdateEvent) => void; ydoc: Y.Doc; provider: SocketIOProvider; } -export function Editor({ onEditorUpdate, ydoc, provider }: EditorProp) { +export function Editor({ ydoc, provider }: EditorProp) { const { openNode, openColor, @@ -81,7 +75,6 @@ export function Editor({ onEditorUpdate, ydoc, provider }: EditorProp) { }, }} slotAfter={} - onUpdate={onEditorUpdate} > diff --git a/apps/frontend/src/features/editor/ui/EditorActionPanel/index.tsx b/apps/frontend/src/features/editor/ui/EditorActionPanel/index.tsx index a87983e9..718b949c 100644 --- a/apps/frontend/src/features/editor/ui/EditorActionPanel/index.tsx +++ b/apps/frontend/src/features/editor/ui/EditorActionPanel/index.tsx @@ -6,14 +6,9 @@ import { } from "lucide-react"; import { useEditorStore } from "../../model/editorStore"; -import SaveStatus from "../SaveStatus"; import { cn } from "@/shared/lib"; -interface EditorActionPanelProps { - saveStatus: "saved" | "unsaved"; -} - -export function EditorActionPanel({ saveStatus }: EditorActionPanelProps) { +export function EditorActionPanel() { const { isPanelOpen, togglePanel, isMaximized, toggleMaximized } = useEditorStore(); @@ -51,7 +46,6 @@ export function EditorActionPanel({ saveStatus }: EditorActionPanelProps) { )} - ); diff --git a/apps/frontend/src/features/editor/ui/SaveStatus/index.tsx b/apps/frontend/src/features/editor/ui/SaveStatus/index.tsx deleted file mode 100644 index 8f355c73..00000000 --- a/apps/frontend/src/features/editor/ui/SaveStatus/index.tsx +++ /dev/null @@ -1,11 +0,0 @@ -export interface SaveStatusProps { - saveStatus: "saved" | "unsaved"; -} - -export default function SaveStatus({ saveStatus }: SaveStatusProps) { - return ( -
- {saveStatus} -
- ); -} diff --git a/apps/frontend/src/widgets/EditorView/model/useEditorView.ts b/apps/frontend/src/widgets/EditorView/model/useEditorView.ts index fd197093..537bf3d2 100644 --- a/apps/frontend/src/widgets/EditorView/model/useEditorView.ts +++ b/apps/frontend/src/widgets/EditorView/model/useEditorView.ts @@ -1,5 +1,4 @@ -import { useEffect, useState } from "react"; -import { useDebouncedCallback } from "use-debounce"; +import { useEffect } from "react"; import { useUserStore } from "@/entities/user"; import { usePageStore } from "@/entities/page"; @@ -10,7 +9,6 @@ import { useEdtorConnection } from "@/features/editor/model/useEditorConnection" export const useEditorView = () => { const { currentPage } = usePageStore(); const { isPanelOpen, isMaximized, setIsPanelOpen } = useEditorStore(); - const [saveStatus, setSaveStatus] = useState<"saved" | "unsaved">("saved"); useEdtorConnection(currentPage); const { editor } = useConnectionStore(); const { users } = useUserStore(); @@ -25,22 +23,12 @@ export const useEditorView = () => { setIsPanelOpen(true); }, [currentPage]); - const handleEditorUpdate = useDebouncedCallback(async () => { - if (currentPage === null) { - return; - } - - setSaveStatus("unsaved"); - }, 500); - return { currentPage, isPanelOpen, isMaximized, ydoc: editor.provider?.doc, provider: editor.provider, - saveStatus, - handleEditorUpdate, users, }; }; diff --git a/apps/frontend/src/widgets/EditorView/ui/index.tsx b/apps/frontend/src/widgets/EditorView/ui/index.tsx index 52af1acd..24918d92 100644 --- a/apps/frontend/src/widgets/EditorView/ui/index.tsx +++ b/apps/frontend/src/widgets/EditorView/ui/index.tsx @@ -4,15 +4,8 @@ import { ActiveUser } from "@/shared/ui"; import { cn } from "@/shared/lib"; export function EditorView() { - const { - currentPage, - isPanelOpen, - isMaximized, - provider, - saveStatus, - handleEditorUpdate, - users, - } = useEditorView(); + const { currentPage, isPanelOpen, isMaximized, provider, users } = + useEditorView(); if (currentPage === null) { return null; @@ -28,7 +21,7 @@ export function EditorView() { isMaximized ? "right-0 top-0 h-screen w-screen" : "", )} > - +
From 7aa82723117f96352496965b6de0c8708af03170 Mon Sep 17 00:00:00 2001 From: mssak Date: Wed, 8 Jan 2025 16:33:19 +0900 Subject: [PATCH 02/26] =?UTF-8?q?refactor:=20websocket=20=EB=94=94?= =?UTF-8?q?=EB=A0=89=ED=86=A0=EB=A6=AC=20=EB=82=B4=EB=B6=80=20setField=20?= =?UTF-8?q?=EB=AA=A8=EB=91=90=20setFields=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/backend/.eslintrc.js | 6 ++ apps/websocket/.eslintrc.js | 6 ++ apps/websocket/src/redis/redis.service.ts | 22 ++++- apps/websocket/src/yjs/yjs.service.ts | 109 ++++++++++++---------- 4 files changed, 94 insertions(+), 49 deletions(-) diff --git a/apps/backend/.eslintrc.js b/apps/backend/.eslintrc.js index 259de13c..465fcf8d 100644 --- a/apps/backend/.eslintrc.js +++ b/apps/backend/.eslintrc.js @@ -22,4 +22,10 @@ module.exports = { '@typescript-eslint/explicit-module-boundary-types': 'off', '@typescript-eslint/no-explicit-any': 'off', }, + 'prettier/prettier': [ + 'error', + { + endOfLine: 'auto', + }, + ], }; diff --git a/apps/websocket/.eslintrc.js b/apps/websocket/.eslintrc.js index 259de13c..465fcf8d 100644 --- a/apps/websocket/.eslintrc.js +++ b/apps/websocket/.eslintrc.js @@ -22,4 +22,10 @@ module.exports = { '@typescript-eslint/explicit-module-boundary-types': 'off', '@typescript-eslint/no-explicit-any': 'off', }, + 'prettier/prettier': [ + 'error', + { + endOfLine: 'auto', + }, + ], }; diff --git a/apps/websocket/src/redis/redis.service.ts b/apps/websocket/src/redis/redis.service.ts index 62253590..adcd0f18 100644 --- a/apps/websocket/src/redis/redis.service.ts +++ b/apps/websocket/src/redis/redis.service.ts @@ -42,11 +42,29 @@ export class RedisService { } } - async setField(key: string, field: string, value: string) { + // async setField(key: string, field: string, value: string) { + // // 락을 획득할 때까지 기다린다. + // const lock = await this.redisLock.acquire([`user:${key}`], 1000); + // try { + // return await this.redisClient.hset(key, field, value); + // } finally { + // lock.release(); + // } + // } + + async setFields(key: string, map: Record) { // 락을 획득할 때까지 기다린다. const lock = await this.redisLock.acquire([`user:${key}`], 1000); try { - return await this.redisClient.hset(key, field, value); + // return await this.redisClient.hset(key, ); + // fieldValueArr 배열을 평탄화하여 [field, value, field, value, ...] 형태로 변환 + const flattenedFields = Object.entries(map).flatMap(([field, value]) => [ + field, + value, + ]); + + // hset을 통해 한 번에 여러 필드를 설정 + return await this.redisClient.hset(key, ...flattenedFields); } finally { lock.release(); } diff --git a/apps/websocket/src/yjs/yjs.service.ts b/apps/websocket/src/yjs/yjs.service.ts index ff4dad8f..dd848252 100644 --- a/apps/websocket/src/yjs/yjs.service.ts +++ b/apps/websocket/src/yjs/yjs.service.ts @@ -247,20 +247,18 @@ export class YjsService private async observeTitle(event: Y.YEvent[]) { // path가 존재할 때만 페이지 갱신 event[0].path.toString().split('_')[1] && - this.redisService.setField( + (await this.redisService.setFields( `page:${event[0].path.toString().split('_')[1]}`, - 'title', - event[0].target.toString(), - ); + { title: event[0].target.toString() }, + )); } private async observeEmoji(event: Y.YEvent[]) { // path가 존재할 때만 페이지 갱신 event[0].path.toString().split('_')[1] && - this.redisService.setField( + this.redisService.setFields( `page:${event[0].path.toString().split('_')[1]}`, - 'emoji', - event[0].target.toString(), + { emoji: event[0].target.toString() }, ); } @@ -288,15 +286,20 @@ export class YjsService const findPage = pageResponse.data.page; - await Promise.all([ - this.redisService.setField(`node:${findPage.node.id}`, 'x', x), - this.redisService.setField(`node:${findPage.node.id}`, 'y', y), - this.redisService.setField( - `node:${findPage.node.id}`, - 'color', - color, - ), - ]); + await this.redisService.setFields(`node:${findPage.node.id}`, { + x, + y, + color, + }); + // await Promise.all([ + // this.redisService.setField(`node:${findPage.node.id}`, 'x', x), + // this.redisService.setField(`node:${findPage.node.id}`, 'y', y), + // this.redisService.setField( + // `node:${findPage.node.id}`, + // 'color', + // color, + // ), + // ]); } catch (error) { this.logger.error( `노드 업데이트 중 오류 발생 (nodeId: ${id}): ${error.message}`, @@ -319,39 +322,48 @@ export class YjsService if (change.action === 'add') { // 연결된 노드가 없을 때만 edge 생성 - this.redisService.setField( - `edge:${edge.source}-${edge.target}`, - 'fromNode', - edge.source, - ); - this.redisService.setField( + await this.redisService.setFields( `edge:${edge.source}-${edge.target}`, - 'toNode', - edge.target, - ); - this.redisService.setField( - `edge:${edge.source}-${edge.target}`, - 'type', - 'add', + { fromNode: edge.source, toNode: edge.target, type: 'add' }, ); + // this.redisService.setField( + // `edge:${edge.source}-${edge.target}`, + // 'fromNode', + // edge.source, + // ); + // this.redisService.setField( + // `edge:${edge.source}-${edge.target}`, + // 'toNode', + // edge.target, + // ); + // this.redisService.setField( + // `edge:${edge.source}-${edge.target}`, + // 'type', + // 'add', + // ); } if (change.action === 'delete') { // 엣지가 존재하면 삭제 - this.redisService.setField( - `edge:${fromNode}-${toNode}`, - 'fromNode', + await this.redisService.setFields(`edge:${fromNode}-${toNode}`, { fromNode, - ); - this.redisService.setField( - `edge:${fromNode}-${toNode}`, - 'toNode', toNode, - ); - this.redisService.setField( - `edge:${fromNode}-${toNode}`, - 'type', - 'delete', - ); + type: 'delete', + }); + // this.redisService.setField( + // `edge:${fromNode}-${toNode}`, + // 'fromNode', + // fromNode, + // ); + // this.redisService.setField( + // `edge:${fromNode}-${toNode}`, + // 'toNode', + // toNode, + // ); + // this.redisService.setField( + // `edge:${fromNode}-${toNode}`, + // 'type', + // 'delete', + // ); } } } @@ -361,11 +373,14 @@ export class YjsService try { const pageId = parseInt(document.name.split('-')[1]); - await this.redisService.setField( - `page:${pageId.toString()}`, - 'content', - JSON.stringify(yXmlFragmentToProsemirrorJSON(editorDoc)), - ); + await this.redisService.setFields(`page:${pageId.toString()}`, { + content: JSON.stringify(yXmlFragmentToProsemirrorJSON(editorDoc)), + }); + // await this.redisService.setField( + // `page:${pageId.toString()}`, + // 'content', + // JSON.stringify(yXmlFragmentToProsemirrorJSON(editorDoc)), + // ); } catch (error) { this.logger.error( `에디터 내용 저장 중 오류 발생 (pageId: ${document?.name}): ${error.message}`, From 72d1bb77065b326251b4b569107d1ff171494dd3 Mon Sep 17 00:00:00 2001 From: mssak Date: Wed, 8 Jan 2025 17:15:20 +0900 Subject: [PATCH 03/26] =?UTF-8?q?refactor:=20redis=20=ED=8A=B8=EB=9E=9C?= =?UTF-8?q?=EC=A0=9D=EC=85=98=20=EB=B0=A9=EC=8B=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/backend/src/redis/redis.service.ts | 6 +- apps/backend/src/tasks/tasks.service.ts | 95 ++++++++++++++--------- apps/websocket/src/redis/redis.service.ts | 6 +- 3 files changed, 64 insertions(+), 43 deletions(-) diff --git a/apps/backend/src/redis/redis.service.ts b/apps/backend/src/redis/redis.service.ts index de1a41f3..57341b9a 100644 --- a/apps/backend/src/redis/redis.service.ts +++ b/apps/backend/src/redis/redis.service.ts @@ -18,9 +18,9 @@ export type RedisNode = { }; export type RedisEdge = { - fromNode: number; - toNode: number; - type: 'add' | 'delete'; + fromNode?: number; + toNode?: number; + type?: 'add' | 'delete'; }; @Injectable() diff --git a/apps/backend/src/tasks/tasks.service.ts b/apps/backend/src/tasks/tasks.service.ts index b74a6a90..18f964fc 100644 --- a/apps/backend/src/tasks/tasks.service.ts +++ b/apps/backend/src/tasks/tasks.service.ts @@ -11,12 +11,16 @@ import { InjectDataSource } from '@nestjs/typeorm'; import { Page } from '../page/page.entity'; import { Node } from '../node/node.entity'; import { Edge } from '../edge/edge.entity'; +import { Inject } from '@nestjs/common'; +import Redis from 'ioredis'; + +const REDIS_CLIENT_TOKEN = 'REDIS_CLIENT'; @Injectable() export class TasksService { private readonly logger = new Logger(TasksService.name); constructor( - private readonly redisService: RedisService, + @Inject(REDIS_CLIENT_TOKEN) private readonly redisClient: Redis, @InjectDataSource() private readonly dataSource: DataSource, ) {} @@ -25,9 +29,13 @@ export class TasksService { this.logger.log('스케줄러 시작'); // 시작 시간 const startTime = performance.now(); - const pageKeys = await this.redisService.getAllKeys('page:*'); - const nodeKeys = await this.redisService.getAllKeys('node:*'); - const edgeKeys = await this.redisService.getAllKeys('edge:*'); + + const pageKeys = await this.redisClient.keys('page:*'); + const nodeKeys = await this.redisClient.keys('node:*'); + const edgeKeys = await this.redisClient.keys('edge:*'); + // const pageKeys = await this.redisService.getAllKeys('page:*'); + // const nodeKeys = await this.redisService.getAllKeys('node:*'); + // const edgeKeys = await this.redisService.getAllKeys('edge:*'); Promise.allSettled([ ...pageKeys.map(this.migratePage.bind(this)), @@ -51,7 +59,11 @@ export class TasksService { } async migratePage(key: string) { - const redisData = (await this.redisService.get(key)) as RedisPage; + const data = await this.redisClient.hgetall(key); + const redisData = Object.fromEntries( + Object.entries(data).map(([field, value]) => [field, value]), + ) as RedisPage; + // const redisData = (await this.redisClient.hgetall(key)) as RedisPage; // 데이터 없으면 오류 if (!redisData) { throw new Error(`redis에 ${key}에 해당하는 데이터가 없습니다.`); @@ -71,6 +83,7 @@ export class TasksService { // 트랜잭션 시작 const queryRunner = this.dataSource.createQueryRunner(); + const redisRunner = this.redisClient.multi(); try { await queryRunner.startTransaction(); @@ -81,20 +94,23 @@ export class TasksService { await pageRepository.update(pageId, updateData); // redis에서 데이터 삭제 - await this.redisService.delete(key); + redisRunner.del(key); + // await this.redisService.delete(key); // 트랜잭션 커밋 await queryRunner.commitTransaction(); + await redisRunner.exec(); } catch (err) { // 실패하면 postgres는 roll back하고 redis의 값을 살린다. this.logger.error(err.stack); await queryRunner.rollbackTransaction(); - updateData.title && - (await this.redisService.setField(key, 'title', updateData.title)); - updateData.content && - (await this.redisService.setField(key, 'content', JSON.parse(content))); - updateData.emoji && - (await this.redisService.setField(key, 'emoji', updateData.emoji)); + redisRunner.discard(); + // updateData.title && + // (await this.redisService.setField(key, 'title', updateData.title)); + // updateData.content && + // (await this.redisService.setField(key, 'content', JSON.parse(content))); + // updateData.emoji && + // (await this.redisService.setField(key, 'emoji', updateData.emoji)); // Promise.all에서 실패를 인식하기 위해 에러를 던진다. throw err; @@ -105,9 +121,10 @@ export class TasksService { } async migrateNode(key: string) { - const redisData = (await this.redisService.get( - key, - )) as unknown as RedisNode; + const data = await this.redisClient.hgetall(key); + const redisData = Object.fromEntries( + Object.entries(data).map(([field, value]) => [field, value]), + ) as RedisNode; // 데이터 없으면 오류 if (!redisData) { throw new Error(`redis에 ${key}에 해당하는 데이터가 없습니다.`); @@ -126,6 +143,7 @@ export class TasksService { // 트랜잭션 시작 const queryRunner = this.dataSource.createQueryRunner(); + const redisRunner = this.redisClient.multi(); try { await queryRunner.startTransaction(); @@ -136,20 +154,16 @@ export class TasksService { await nodeRepository.update(nodeId, updateData); // redis에서 데이터 삭제 - await this.redisService.delete(key); + redisRunner.del(key); // 트랜잭션 커밋 await queryRunner.commitTransaction(); + await redisRunner.exec(); } catch (err) { // 실패하면 postgres는 roll back하고 redis의 값을 살린다. this.logger.error(err.stack); await queryRunner.rollbackTransaction(); - updateData.x && - (await this.redisService.setField(key, 'x', updateData.x.toString())); - updateData.y && - (await this.redisService.setField(key, 'y', updateData.y.toString())); - updateData.color && - (await this.redisService.setField(key, 'color', updateData.color)); + redisRunner.discard(); // Promise.all에서 실패를 인식하기 위해 에러를 던진다. throw err; @@ -160,9 +174,11 @@ export class TasksService { } async migrateEdge(key: string) { - const redisData = (await this.redisService.get( - key, - )) as unknown as RedisEdge; + const data = await this.redisClient.hgetall(key); + const redisData = Object.fromEntries( + Object.entries(data).map(([field, value]) => [field, value]), + ) as RedisEdge; + // 데이터 없으면 오류 if (!redisData) { throw new Error(`redis에 ${key}에 해당하는 데이터가 없습니다.`); @@ -170,6 +186,8 @@ export class TasksService { // 트랜잭션 시작 const queryRunner = this.dataSource.createQueryRunner(); + const redisRunner = this.redisClient.multi(); + try { await queryRunner.startTransaction(); @@ -203,7 +221,8 @@ export class TasksService { } // redis에서 데이터 삭제 - await this.redisService.delete(key); + redisRunner.del(key); + // await this.redisService.delete(key); // 트랜잭션 커밋 await queryRunner.commitTransaction(); @@ -211,17 +230,19 @@ export class TasksService { // 실패하면 postgres는 roll back하고 redis의 값을 살린다. this.logger.error(err.stack); await queryRunner.rollbackTransaction(); - await this.redisService.setField( - key, - 'fromNode', - redisData.fromNode.toString(), - ); - await this.redisService.setField( - key, - 'toNode', - redisData.toNode.toString(), - ); - await this.redisService.setField(key, 'type', redisData.type); + redisRunner.discard(); + + // await this.redisService.setField( + // key, + // 'fromNode', + // redisData.fromNode.toString(), + // ); + // await this.redisService.setField( + // key, + // 'toNode', + // redisData.toNode.toString(), + // ); + // await this.redisService.setField(key, 'type', redisData.type); // Promise.all에서 실패를 인식하기 위해 에러를 던진다. throw err; diff --git a/apps/websocket/src/redis/redis.service.ts b/apps/websocket/src/redis/redis.service.ts index adcd0f18..770ad34f 100644 --- a/apps/websocket/src/redis/redis.service.ts +++ b/apps/websocket/src/redis/redis.service.ts @@ -17,9 +17,9 @@ export class RedisService { @Inject(RED_LOCK_TOKEN) private readonly redisLock: Redlock, ) {} - async getAllKeys(pattern) { - return await this.redisClient.keys(pattern); - } + // async getAllKeys(pattern) { + // return await this.redisClient.keys(pattern); + // } createStream() { return this.redisClient.scanStream(); From 1bd2153c145a06c6121c817a6173be9b50711e59 Mon Sep 17 00:00:00 2001 From: pkh0106 Date: Wed, 8 Jan 2025 17:34:37 +0900 Subject: [PATCH 04/26] =?UTF-8?q?refactor:=20useCanvas=20=EB=AF=B8?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=EC=9A=A9=20=EB=B0=98=ED=99=98=EA=B0=92=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/frontend/src/features/canvas/model/useCanvas.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/frontend/src/features/canvas/model/useCanvas.ts b/apps/frontend/src/features/canvas/model/useCanvas.ts index 8fe33000..55430844 100644 --- a/apps/frontend/src/features/canvas/model/useCanvas.ts +++ b/apps/frontend/src/features/canvas/model/useCanvas.ts @@ -326,7 +326,6 @@ export const useCanvas = () => { nodes, edges, users, - setCurrentPage, handleMouseMove, handleNodesChange, handleEdgesChange, From 3fe4308bd0d056bbce721ff95328c9da98df4bf6 Mon Sep 17 00:00:00 2001 From: pkh0106 Date: Wed, 8 Jan 2025 17:41:36 +0900 Subject: [PATCH 05/26] =?UTF-8?q?refactor:=20=EB=B9=88=20=EB=AC=B8?= =?UTF-8?q?=EC=9E=90=EC=97=B4=20=EC=82=AC=EC=9A=A9=20=EC=A0=9C=EA=B1=B0?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/frontend/src/features/canvas/ui/Canvas/index.tsx | 2 +- apps/frontend/src/features/editor/ui/Editor/index.tsx | 1 - apps/frontend/src/shared/ui/Emoji/index.tsx | 4 ++-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/apps/frontend/src/features/canvas/ui/Canvas/index.tsx b/apps/frontend/src/features/canvas/ui/Canvas/index.tsx index ac9b1269..b95c0924 100644 --- a/apps/frontend/src/features/canvas/ui/Canvas/index.tsx +++ b/apps/frontend/src/features/canvas/ui/Canvas/index.tsx @@ -60,7 +60,7 @@ export function Canvas({ className }: CanvasProps) { }, [users]); return ( -
+
{ disableCollaboration(); diff --git a/apps/frontend/src/shared/ui/Emoji/index.tsx b/apps/frontend/src/shared/ui/Emoji/index.tsx index 898ae543..359f5f60 100644 --- a/apps/frontend/src/shared/ui/Emoji/index.tsx +++ b/apps/frontend/src/shared/ui/Emoji/index.tsx @@ -14,11 +14,11 @@ export function Emoji({ emoji, width, height, fontSize }: EmojiProps) { if (!emoji) return ( ); - return
{emoji}
; + return
{emoji}
; } From d5e2e076712bdcd79b44e732c159ab4726f3482c Mon Sep 17 00:00:00 2001 From: pkh0106 Date: Wed, 8 Jan 2025 17:43:47 +0900 Subject: [PATCH 06/26] =?UTF-8?q?refactor:=20=EB=82=B4=EB=B6=80=EC=97=90?= =?UTF-8?q?=EC=84=9C=EB=A7=8C=20=EC=82=AC=EC=9A=A9=ED=95=98=EB=8A=94=20?= =?UTF-8?q?=EB=AA=A8=EB=93=88=20export=20=EC=A0=9C=EA=B1=B0=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/frontend/src/features/canvas/model/calculateHandles.ts | 2 +- .../features/editor/ui/Editor/selectors/color-selector.tsx | 2 +- .../src/features/editor/ui/Editor/selectors/link-selector.tsx | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/frontend/src/features/canvas/model/calculateHandles.ts b/apps/frontend/src/features/canvas/model/calculateHandles.ts index 71655bb4..43f032c6 100644 --- a/apps/frontend/src/features/canvas/model/calculateHandles.ts +++ b/apps/frontend/src/features/canvas/model/calculateHandles.ts @@ -1,6 +1,6 @@ import { Position, Node } from "@xyflow/react"; -export const getHandlePosition = (node: Node, handleId: Position) => { +const getHandlePosition = (node: Node, handleId: Position) => { const nodeElement = document.querySelector(`[data-id="${node.id}"]`); const nodeRect = nodeElement!.getBoundingClientRect(); const nodeWidth = nodeRect.width; diff --git a/apps/frontend/src/features/editor/ui/Editor/selectors/color-selector.tsx b/apps/frontend/src/features/editor/ui/Editor/selectors/color-selector.tsx index f5ebf3e4..bd5ee44d 100644 --- a/apps/frontend/src/features/editor/ui/Editor/selectors/color-selector.tsx +++ b/apps/frontend/src/features/editor/ui/Editor/selectors/color-selector.tsx @@ -4,7 +4,7 @@ import { EditorBubbleItem, useEditor } from "novel"; import { Button } from "../ui/button"; import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover"; -export interface BubbleColorMenuItem { +interface BubbleColorMenuItem { name: string; color: string; } diff --git a/apps/frontend/src/features/editor/ui/Editor/selectors/link-selector.tsx b/apps/frontend/src/features/editor/ui/Editor/selectors/link-selector.tsx index 6fcae9c0..c9c5cfec 100644 --- a/apps/frontend/src/features/editor/ui/Editor/selectors/link-selector.tsx +++ b/apps/frontend/src/features/editor/ui/Editor/selectors/link-selector.tsx @@ -7,7 +7,7 @@ import { Button } from "../ui/button"; import { PopoverContent } from "../ui/popover"; import { cn } from "@/shared/lib"; -export function isValidUrl(url: string) { +function isValidUrl(url: string) { try { new URL(url); return true; @@ -15,7 +15,7 @@ export function isValidUrl(url: string) { return false; } } -export function getUrlFromString(str: string) { +function getUrlFromString(str: string) { if (isValidUrl(str)) return str; try { if (str.includes(".") && !str.includes(" ")) { From 860d156eed20421f47d63632f26b43601880e4a9 Mon Sep 17 00:00:00 2001 From: pkh0106 Date: Wed, 8 Jan 2025 17:49:50 +0900 Subject: [PATCH 07/26] =?UTF-8?q?refactor:=20page=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=EB=AF=B8=EC=82=AC=EC=9A=A9=20API=20=EC=A0=9C=EA=B1=B0=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../frontend/src/entities/page/api/pageApi.ts | 32 ++----------------- apps/frontend/src/entities/page/index.ts | 8 +---- .../src/entities/page/model/pageTypes.ts | 16 ---------- 3 files changed, 3 insertions(+), 53 deletions(-) diff --git a/apps/frontend/src/entities/page/api/pageApi.ts b/apps/frontend/src/entities/page/api/pageApi.ts index dccb3a06..42828580 100644 --- a/apps/frontend/src/entities/page/api/pageApi.ts +++ b/apps/frontend/src/entities/page/api/pageApi.ts @@ -1,26 +1,5 @@ -import { Get, Post, Delete, Patch } from "@/shared/api"; -import { - GetPageResponse, - GetPagesResponse, - CreatePageRequest, - CreatePageResponse, - UpdatePageRequest, -} from "../model/pageTypes"; - -export const getPage = async (id: number) => { - const url = `/api/page/${id}`; - - const res = await Get(url); - return res.data.page; -}; - -// TODO: 임시 -export const getPages = async (workspaceId: string) => { - const url = `/api/page/workspace/${workspaceId}`; - - const res = await Get(url); - return res.data.pages; -}; +import { Post, Delete } from "@/shared/api"; +import { CreatePageRequest, CreatePageResponse } from "../model/pageTypes"; export const createPage = async (pageData: CreatePageRequest) => { const url = `/api/page`; @@ -35,10 +14,3 @@ export const deletePage = async (id: number) => { const res = await Delete(url); return res.data; }; - -export const updatePage = async (id: number, pageData: UpdatePageRequest) => { - const url = `/api/page/${id}`; - - const res = await Patch(url, pageData); - return res.data; -}; diff --git a/apps/frontend/src/entities/page/index.ts b/apps/frontend/src/entities/page/index.ts index 6fcbb829..d6381d2b 100644 --- a/apps/frontend/src/entities/page/index.ts +++ b/apps/frontend/src/entities/page/index.ts @@ -1,10 +1,4 @@ -export { - getPage, - getPages, - createPage, - deletePage, - updatePage, -} from "./api/pageApi"; +export { createPage, deletePage } from "./api/pageApi"; export { useCreatePage, useDeletePage } from "./model/pageMutations"; export { usePageStore } from "./model/pageStore"; diff --git a/apps/frontend/src/entities/page/model/pageTypes.ts b/apps/frontend/src/entities/page/model/pageTypes.ts index 779ce901..2e42860a 100644 --- a/apps/frontend/src/entities/page/model/pageTypes.ts +++ b/apps/frontend/src/entities/page/model/pageTypes.ts @@ -7,16 +7,6 @@ export interface Page { emoji: string | null; } -export interface GetPageResponse { - message: string; - page: Page; -} - -export interface GetPagesResponse { - message: string; - pages: Omit[]; -} - export interface CreatePageRequest { title: string; content: JSONContent; @@ -30,9 +20,3 @@ export interface CreatePageResponse { message: string; pageId: number; } - -export interface UpdatePageRequest { - title: string; - content: JSONContent; - emoji: string | null; -} From af7ca2ad058b9040d493f32c00a440e6c0131472 Mon Sep 17 00:00:00 2001 From: pkh0106 Date: Wed, 8 Jan 2025 17:52:11 +0900 Subject: [PATCH 08/26] =?UTF-8?q?refactor:=20Separator=20default=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - horizontal -> vertical --- apps/frontend/src/features/editor/ui/Editor/index.tsx | 10 +++++----- .../src/features/editor/ui/Editor/ui/separator.tsx | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/frontend/src/features/editor/ui/Editor/index.tsx b/apps/frontend/src/features/editor/ui/Editor/index.tsx index cd2c85ca..5f3d7035 100644 --- a/apps/frontend/src/features/editor/ui/Editor/index.tsx +++ b/apps/frontend/src/features/editor/ui/Editor/index.tsx @@ -106,15 +106,15 @@ export function Editor({ ydoc, provider }: EditorProp) { }} className="flex w-fit max-w-[90vw] overflow-hidden rounded-md border border-muted bg-background shadow-xl" > - + - + - + - + - +
diff --git a/apps/frontend/src/features/editor/ui/Editor/ui/separator.tsx b/apps/frontend/src/features/editor/ui/Editor/ui/separator.tsx index e84f76bd..340ca8d2 100644 --- a/apps/frontend/src/features/editor/ui/Editor/ui/separator.tsx +++ b/apps/frontend/src/features/editor/ui/Editor/ui/separator.tsx @@ -10,7 +10,7 @@ const Separator = React.forwardRef< React.ComponentPropsWithoutRef >( ( - { className, orientation = "horizontal", decorative = true, ...props }, + { className, orientation = "vertical", decorative = true, ...props }, ref, ) => ( Date: Wed, 8 Jan 2025 17:54:38 +0900 Subject: [PATCH 09/26] =?UTF-8?q?refactor:=20useEffect=20=EC=A4=91?= =?UTF-8?q?=EB=B3=B5=20=EB=A1=9C=EC=A7=81=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 동일한 의존성을 가지고, 같은 의미의 로직을 수행하므로 합침 --- .../src/widgets/EditorView/model/useEditorView.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/apps/frontend/src/widgets/EditorView/model/useEditorView.ts b/apps/frontend/src/widgets/EditorView/model/useEditorView.ts index 537bf3d2..ce5dd52c 100644 --- a/apps/frontend/src/widgets/EditorView/model/useEditorView.ts +++ b/apps/frontend/src/widgets/EditorView/model/useEditorView.ts @@ -14,13 +14,7 @@ export const useEditorView = () => { const { users } = useUserStore(); useEffect(() => { - if (currentPage) return; - setIsPanelOpen(false); - }, [currentPage]); - - useEffect(() => { - if (!currentPage) return; - setIsPanelOpen(true); + setIsPanelOpen(!!currentPage); }, [currentPage]); return { From eff8680d27e10fd1120c5f7f9fa76b4fa583adbd Mon Sep 17 00:00:00 2001 From: baegyeong Date: Wed, 8 Jan 2025 19:49:03 +0900 Subject: [PATCH 10/26] =?UTF-8?q?refactor:=20TODO=20=EC=A3=BC=EC=84=9D=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 논의가 필요한 TODO는 노션에 따로 표시 --- apps/frontend/src/features/editor/index.ts | 1 - apps/frontend/src/features/editor/model/upload.ts | 1 - apps/frontend/src/features/editor/ui/Editor/extensions.ts | 4 ---- apps/frontend/src/features/pageSidebar/model/useNoteList.ts | 1 - apps/frontend/src/features/workspace/api/workspaceApi.ts | 2 -- .../workspace/ui/WorkspaceList/WorkspaceRemoveModal/index.tsx | 1 - 6 files changed, 10 deletions(-) diff --git a/apps/frontend/src/features/editor/index.ts b/apps/frontend/src/features/editor/index.ts index 973d19e4..a36637d1 100644 --- a/apps/frontend/src/features/editor/index.ts +++ b/apps/frontend/src/features/editor/index.ts @@ -2,7 +2,6 @@ export { onUploadImage } from "./api/uploadApi"; export { useEditorStore } from "./model/editorStore"; -// TODO: 정리 export { Editor } from "@/features/editor/ui/Editor"; export { EditorTitle } from "@/features/editor/ui/EditorTitle"; export { EditorActionPanel } from "@/features/editor/ui/EditorActionPanel"; diff --git a/apps/frontend/src/features/editor/model/upload.ts b/apps/frontend/src/features/editor/model/upload.ts index 785a5813..92aa8c5a 100644 --- a/apps/frontend/src/features/editor/model/upload.ts +++ b/apps/frontend/src/features/editor/model/upload.ts @@ -2,7 +2,6 @@ import { createImageUpload } from "novel/plugins"; import { onUploadImage } from "../api/uploadApi"; -// TODO: validateFn 수정해야할듯? export const uploadFn = createImageUpload({ onUpload: onUploadImage, validateFn: (file) => { diff --git a/apps/frontend/src/features/editor/ui/Editor/extensions.ts b/apps/frontend/src/features/editor/ui/Editor/extensions.ts index 1a4b3d84..f9f834f4 100644 --- a/apps/frontend/src/features/editor/ui/Editor/extensions.ts +++ b/apps/frontend/src/features/editor/ui/Editor/extensions.ts @@ -25,9 +25,7 @@ import { UploadImagesPlugin } from "novel/plugins"; import { cx } from "class-variance-authority"; import { common, createLowlight } from "lowlight"; -//TODO I am using cx here to get tailwind autocomplete working, idk if someone else can write a regex to just capture the class key in objects const aiHighlight = AIHighlight; -//You can overwrite the placeholder with your own configuration const placeholder = Placeholder; const tiptapLink = TiptapLink.configure({ HTMLAttributes: { @@ -119,8 +117,6 @@ const starterKit = StarterKit.configure({ }); const codeBlockLowlight = CodeBlockLowlight.configure({ - // configure lowlight: common / all / use highlightJS in case there is a need to specify certain language grammars only - // common: covers 37 language grammars which should be good enough in most cases lowlight: createLowlight(common), }); diff --git a/apps/frontend/src/features/pageSidebar/model/useNoteList.ts b/apps/frontend/src/features/pageSidebar/model/useNoteList.ts index b40a7fe9..93e626b7 100644 --- a/apps/frontend/src/features/pageSidebar/model/useNoteList.ts +++ b/apps/frontend/src/features/pageSidebar/model/useNoteList.ts @@ -10,7 +10,6 @@ export const useNoteList = () => { const [pages, setPages] = useState(); const { canvas } = useConnectionStore(); - // TODO: 최적화 필요 useEffect(() => { if (!canvas.provider) return; const nodesMap = canvas.provider.doc.getMap("nodes"); diff --git a/apps/frontend/src/features/workspace/api/workspaceApi.ts b/apps/frontend/src/features/workspace/api/workspaceApi.ts index fb304b9a..c1352f80 100644 --- a/apps/frontend/src/features/workspace/api/workspaceApi.ts +++ b/apps/frontend/src/features/workspace/api/workspaceApi.ts @@ -24,7 +24,6 @@ export const removeWorkspace = async (workspaceId: string) => { return res.data; }; -// TODO: /entities/user vs workspace 위치 고민해봐야할듯? export const getUserWorkspaces = async () => { const url = `${BASE_URL}/user`; @@ -38,7 +37,6 @@ export const getCurrentWorkspace = async ( ) => { const url = `${BASE_URL}/${workspaceId}/${userId}`; - // Response type 바꾸기 const res = await Get(url); return res.data; }; diff --git a/apps/frontend/src/features/workspace/ui/WorkspaceList/WorkspaceRemoveModal/index.tsx b/apps/frontend/src/features/workspace/ui/WorkspaceList/WorkspaceRemoveModal/index.tsx index 6f3c1ebb..b1e0ebf1 100644 --- a/apps/frontend/src/features/workspace/ui/WorkspaceList/WorkspaceRemoveModal/index.tsx +++ b/apps/frontend/src/features/workspace/ui/WorkspaceList/WorkspaceRemoveModal/index.tsx @@ -7,7 +7,6 @@ type WorkspaceRemoveModalProps = { onCloseModal: () => void; }; -// TODO: RemoveModal도 리팩토링해도 될듯? export function WorkspaceRemoveModal({ isOpen, onConfirm, From bc6a5680df54ffa7edaa39370d18a71cee2e0e54 Mon Sep 17 00:00:00 2001 From: baegyeong Date: Wed, 8 Jan 2025 19:49:30 +0900 Subject: [PATCH 11/26] =?UTF-8?q?refactor:=20=ED=95=84=EC=9A=94=EC=97=86?= =?UTF-8?q?=EB=8A=94=20=EC=A3=BC=EC=84=9D=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../entities/user/model/useUserConnection.ts | 1 - .../ui/Editor/selectors/node-selector.tsx | 1 - .../src/features/editor/ui/Editor/ui/menu.tsx | 38 +------------------ .../workspace/model/workspaceMutations.ts | 2 - 4 files changed, 1 insertion(+), 41 deletions(-) diff --git a/apps/frontend/src/entities/user/model/useUserConnection.ts b/apps/frontend/src/entities/user/model/useUserConnection.ts index ac9e67cb..6e2483c1 100644 --- a/apps/frontend/src/entities/user/model/useUserConnection.ts +++ b/apps/frontend/src/entities/user/model/useUserConnection.ts @@ -1,4 +1,3 @@ -// provider: createSocketIOProvider("users", new Y.Doc()), import * as Y from "yjs"; import { createSocketIOProvider } from "@/shared/api"; import useConnectionStore from "@/shared/model/useConnectionStore"; diff --git a/apps/frontend/src/features/editor/ui/Editor/selectors/node-selector.tsx b/apps/frontend/src/features/editor/ui/Editor/selectors/node-selector.tsx index 0655e740..f620ec05 100644 --- a/apps/frontend/src/features/editor/ui/Editor/selectors/node-selector.tsx +++ b/apps/frontend/src/features/editor/ui/Editor/selectors/node-selector.tsx @@ -21,7 +21,6 @@ const items: SelectorItem[] = [ name: "Text", icon: TextIcon, command: (editor) => editor.chain().focus().clearNodes().run(), - // I feel like there has to be a more efficient way to do this – feel free to PR if you know how! isActive: (editor) => editor.isActive("paragraph") && !editor.isActive("bulletList") && diff --git a/apps/frontend/src/features/editor/ui/Editor/ui/menu.tsx b/apps/frontend/src/features/editor/ui/Editor/ui/menu.tsx index 6a661e7a..d5eb6330 100644 --- a/apps/frontend/src/features/editor/ui/Editor/ui/menu.tsx +++ b/apps/frontend/src/features/editor/ui/Editor/ui/menu.tsx @@ -5,21 +5,6 @@ import { useTheme } from "next-themes"; import { Button } from "./button"; import { Popover, PopoverContent, PopoverTrigger } from "./popover"; -// TODO implement multiple fonts editor -// const fonts = [ -// { -// font: "Default", -// icon: , -// }, -// { -// font: "Serif", -// icon: , -// }, -// { -// font: "Mono", -// icon: , -// }, -// ]; const appearances = [ { theme: "System", @@ -35,7 +20,6 @@ const appearances = [ }, ]; export default function Menu() { - // const { font: currentFont, setFont } = useContext(AppContext); const { theme: currentTheme, setTheme } = useTheme(); return ( @@ -46,27 +30,7 @@ export default function Menu() { - {/*
-

Font

- {fonts.map(({ font, icon }) => ( - - ))} -
*/} -

+

Appearance

{appearances.map(({ theme, icon }) => ( diff --git a/apps/frontend/src/features/workspace/model/workspaceMutations.ts b/apps/frontend/src/features/workspace/model/workspaceMutations.ts index 9e52b1f8..988b1ef9 100644 --- a/apps/frontend/src/features/workspace/model/workspaceMutations.ts +++ b/apps/frontend/src/features/workspace/model/workspaceMutations.ts @@ -11,8 +11,6 @@ import { } from "../api/worskspaceInviteApi"; import { useWorkspace } from "@/shared/lib"; -// response로 workspaceId가 오는데 userWorkspace를 어떻게 invalidate 할까? -// login state에 있는 userId로? export const useCreateWorkspace = () => { const queryClient = useQueryClient(); From c9a832a7015792781e548b4cf07ce712fcaf9a77 Mon Sep 17 00:00:00 2001 From: mssak Date: Thu, 9 Jan 2025 13:23:45 +0900 Subject: [PATCH 12/26] =?UTF-8?q?fix:=20=EC=8A=A4=EC=BC=80=EC=A5=B4?= =?UTF-8?q?=EB=9F=AC=20=EB=8F=8C=EB=95=8C=20redis=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=EA=B0=80=20=EC=A0=9C=EB=8C=80=EB=A1=9C=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=EB=90=98=EC=A7=80=20=EC=95=8A=EB=8A=94=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=20=EC=88=98=EC=A0=95=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Summer Min Co-authored-by: ez --- apps/backend/src/tasks/tasks.service.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/backend/src/tasks/tasks.service.ts b/apps/backend/src/tasks/tasks.service.ts index 18f964fc..09afcdff 100644 --- a/apps/backend/src/tasks/tasks.service.ts +++ b/apps/backend/src/tasks/tasks.service.ts @@ -226,6 +226,7 @@ export class TasksService { // 트랜잭션 커밋 await queryRunner.commitTransaction(); + await redisRunner.exec(); } catch (err) { // 실패하면 postgres는 roll back하고 redis의 값을 살린다. this.logger.error(err.stack); From ea4f32497ff3ae3b22fdedcbcf248f797274161a Mon Sep 17 00:00:00 2001 From: mssak Date: Thu, 9 Jan 2025 13:34:48 +0900 Subject: [PATCH 13/26] =?UTF-8?q?chore:=20=EC=A3=BC=EC=84=9D=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/backend/src/tasks/tasks.service.ts | 24 ------------- apps/websocket/src/redis/redis.service.ts | 14 -------- apps/websocket/src/yjs/yjs.service.ts | 44 ----------------------- 3 files changed, 82 deletions(-) diff --git a/apps/backend/src/tasks/tasks.service.ts b/apps/backend/src/tasks/tasks.service.ts index 09afcdff..a91b75c9 100644 --- a/apps/backend/src/tasks/tasks.service.ts +++ b/apps/backend/src/tasks/tasks.service.ts @@ -33,9 +33,6 @@ export class TasksService { const pageKeys = await this.redisClient.keys('page:*'); const nodeKeys = await this.redisClient.keys('node:*'); const edgeKeys = await this.redisClient.keys('edge:*'); - // const pageKeys = await this.redisService.getAllKeys('page:*'); - // const nodeKeys = await this.redisService.getAllKeys('node:*'); - // const edgeKeys = await this.redisService.getAllKeys('edge:*'); Promise.allSettled([ ...pageKeys.map(this.migratePage.bind(this)), @@ -63,7 +60,6 @@ export class TasksService { const redisData = Object.fromEntries( Object.entries(data).map(([field, value]) => [field, value]), ) as RedisPage; - // const redisData = (await this.redisClient.hgetall(key)) as RedisPage; // 데이터 없으면 오류 if (!redisData) { throw new Error(`redis에 ${key}에 해당하는 데이터가 없습니다.`); @@ -95,7 +91,6 @@ export class TasksService { // redis에서 데이터 삭제 redisRunner.del(key); - // await this.redisService.delete(key); // 트랜잭션 커밋 await queryRunner.commitTransaction(); @@ -105,12 +100,6 @@ export class TasksService { this.logger.error(err.stack); await queryRunner.rollbackTransaction(); redisRunner.discard(); - // updateData.title && - // (await this.redisService.setField(key, 'title', updateData.title)); - // updateData.content && - // (await this.redisService.setField(key, 'content', JSON.parse(content))); - // updateData.emoji && - // (await this.redisService.setField(key, 'emoji', updateData.emoji)); // Promise.all에서 실패를 인식하기 위해 에러를 던진다. throw err; @@ -222,7 +211,6 @@ export class TasksService { // redis에서 데이터 삭제 redisRunner.del(key); - // await this.redisService.delete(key); // 트랜잭션 커밋 await queryRunner.commitTransaction(); @@ -233,18 +221,6 @@ export class TasksService { await queryRunner.rollbackTransaction(); redisRunner.discard(); - // await this.redisService.setField( - // key, - // 'fromNode', - // redisData.fromNode.toString(), - // ); - // await this.redisService.setField( - // key, - // 'toNode', - // redisData.toNode.toString(), - // ); - // await this.redisService.setField(key, 'type', redisData.type); - // Promise.all에서 실패를 인식하기 위해 에러를 던진다. throw err; } finally { diff --git a/apps/websocket/src/redis/redis.service.ts b/apps/websocket/src/redis/redis.service.ts index 770ad34f..718d5330 100644 --- a/apps/websocket/src/redis/redis.service.ts +++ b/apps/websocket/src/redis/redis.service.ts @@ -17,10 +17,6 @@ export class RedisService { @Inject(RED_LOCK_TOKEN) private readonly redisLock: Redlock, ) {} - // async getAllKeys(pattern) { - // return await this.redisClient.keys(pattern); - // } - createStream() { return this.redisClient.scanStream(); } @@ -42,16 +38,6 @@ export class RedisService { } } - // async setField(key: string, field: string, value: string) { - // // 락을 획득할 때까지 기다린다. - // const lock = await this.redisLock.acquire([`user:${key}`], 1000); - // try { - // return await this.redisClient.hset(key, field, value); - // } finally { - // lock.release(); - // } - // } - async setFields(key: string, map: Record) { // 락을 획득할 때까지 기다린다. const lock = await this.redisLock.acquire([`user:${key}`], 1000); diff --git a/apps/websocket/src/yjs/yjs.service.ts b/apps/websocket/src/yjs/yjs.service.ts index dd848252..59000761 100644 --- a/apps/websocket/src/yjs/yjs.service.ts +++ b/apps/websocket/src/yjs/yjs.service.ts @@ -291,15 +291,6 @@ export class YjsService y, color, }); - // await Promise.all([ - // this.redisService.setField(`node:${findPage.node.id}`, 'x', x), - // this.redisService.setField(`node:${findPage.node.id}`, 'y', y), - // this.redisService.setField( - // `node:${findPage.node.id}`, - // 'color', - // color, - // ), - // ]); } catch (error) { this.logger.error( `노드 업데이트 중 오류 발생 (nodeId: ${id}): ${error.message}`, @@ -326,21 +317,6 @@ export class YjsService `edge:${edge.source}-${edge.target}`, { fromNode: edge.source, toNode: edge.target, type: 'add' }, ); - // this.redisService.setField( - // `edge:${edge.source}-${edge.target}`, - // 'fromNode', - // edge.source, - // ); - // this.redisService.setField( - // `edge:${edge.source}-${edge.target}`, - // 'toNode', - // edge.target, - // ); - // this.redisService.setField( - // `edge:${edge.source}-${edge.target}`, - // 'type', - // 'add', - // ); } if (change.action === 'delete') { // 엣지가 존재하면 삭제 @@ -349,21 +325,6 @@ export class YjsService toNode, type: 'delete', }); - // this.redisService.setField( - // `edge:${fromNode}-${toNode}`, - // 'fromNode', - // fromNode, - // ); - // this.redisService.setField( - // `edge:${fromNode}-${toNode}`, - // 'toNode', - // toNode, - // ); - // this.redisService.setField( - // `edge:${fromNode}-${toNode}`, - // 'type', - // 'delete', - // ); } } } @@ -376,11 +337,6 @@ export class YjsService await this.redisService.setFields(`page:${pageId.toString()}`, { content: JSON.stringify(yXmlFragmentToProsemirrorJSON(editorDoc)), }); - // await this.redisService.setField( - // `page:${pageId.toString()}`, - // 'content', - // JSON.stringify(yXmlFragmentToProsemirrorJSON(editorDoc)), - // ); } catch (error) { this.logger.error( `에디터 내용 저장 중 오류 발생 (pageId: ${document?.name}): ${error.message}`, From f475ef84e2845cf438487d6207f197966d119b8b Mon Sep 17 00:00:00 2001 From: mssak Date: Thu, 9 Jan 2025 14:35:10 +0900 Subject: [PATCH 14/26] =?UTF-8?q?chore:=20=EC=9E=84=EC=8B=9C=EB=A1=9C=20ed?= =?UTF-8?q?ge=20delete=20=EA=B8=B0=EB=8A=A5=20=EB=B9=84=ED=99=9C=EC=84=B1?= =?UTF-8?q?=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: ez Co-authored-by: Summer Min --- apps/backend/src/tasks/tasks.service.ts | 17 ++++++++++------- apps/websocket/src/yjs/yjs.service.ts | 1 + 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/apps/backend/src/tasks/tasks.service.ts b/apps/backend/src/tasks/tasks.service.ts index a91b75c9..11e20d71 100644 --- a/apps/backend/src/tasks/tasks.service.ts +++ b/apps/backend/src/tasks/tasks.service.ts @@ -201,13 +201,16 @@ export class TasksService { }); } - if (redisData.type === 'delete') { - const edge = await edgeRepository.findOne({ - where: { fromNode, toNode }, - }); - - await edgeRepository.delete({ id: edge.id }); - } + // if (redisData.type === 'delete') { + // const edge = await edgeRepository.findOne({ + // where: { fromNode, toNode }, + // }); + // console.log(`edge 정보 `); + // console.log(edge); + // console.log(`edge content : ${edge}`); + + // await edgeRepository.delete({ id: edge.id }); + // } // redis에서 데이터 삭제 redisRunner.del(key); diff --git a/apps/websocket/src/yjs/yjs.service.ts b/apps/websocket/src/yjs/yjs.service.ts index 59000761..2a74b927 100644 --- a/apps/websocket/src/yjs/yjs.service.ts +++ b/apps/websocket/src/yjs/yjs.service.ts @@ -309,6 +309,7 @@ export class YjsService ) { for (const [key, change] of event.changes.keys) { const [fromNode, toNode] = key.slice(1).split('-'); + // TODO: 여기서 delete 시 edge를 못찾음 (undefined로 가져옴) const edge = edgesMap.get(key) as YMapEdge; if (change.action === 'add') { From 5feffd9d4cd284a7637e1e2685c588e7c9911717 Mon Sep 17 00:00:00 2001 From: baegyeong Date: Thu, 9 Jan 2025 16:18:35 +0900 Subject: [PATCH 15/26] =?UTF-8?q?refactor:=20EditorView=EC=9D=98=20?= =?UTF-8?q?=EC=8A=A4=EC=BC=88=EB=A0=88=ED=86=A4=20UI=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 박경희 --- apps/frontend/src/shared/ui/Skeleton/index.tsx | 7 +++++++ apps/frontend/src/shared/ui/index.ts | 1 + 2 files changed, 8 insertions(+) create mode 100644 apps/frontend/src/shared/ui/Skeleton/index.tsx diff --git a/apps/frontend/src/shared/ui/Skeleton/index.tsx b/apps/frontend/src/shared/ui/Skeleton/index.tsx new file mode 100644 index 00000000..a35bab61 --- /dev/null +++ b/apps/frontend/src/shared/ui/Skeleton/index.tsx @@ -0,0 +1,7 @@ +export function EditorSkeleton() { + return ( +
+
+
+ ); +} diff --git a/apps/frontend/src/shared/ui/index.ts b/apps/frontend/src/shared/ui/index.ts index f029b8d6..b41f2a23 100644 --- a/apps/frontend/src/shared/ui/index.ts +++ b/apps/frontend/src/shared/ui/index.ts @@ -11,3 +11,4 @@ export { Popover } from "./Popover"; export { ScrollWrapper } from "./ScrollWrapper"; export { SideWrapper } from "./SideWrapper"; export { Switch } from "./Switch"; +export { EditorSkeleton } from "./Skeleton"; From 6b8db17bc3375034eaf5b89ed8380106f11dd788 Mon Sep 17 00:00:00 2001 From: mssak Date: Thu, 9 Jan 2025 16:19:02 +0900 Subject: [PATCH 16/26] =?UTF-8?q?refactor:=20=EB=8F=84=EC=BB=A4=20?= =?UTF-8?q?=EC=B5=9C=EC=A0=81=ED=99=94=20-=20=EC=A4=91=EB=B3=B5=EB=90=98?= =?UTF-8?q?=EB=8A=94=20=EB=B3=BC=EB=A5=A8=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- compose.local.yml | 223 ++++++++++++++++++++++++---------------------- package.json | 56 ++++++------ yarn.lock | 31 ++++++- 3 files changed, 171 insertions(+), 139 deletions(-) diff --git a/compose.local.yml b/compose.local.yml index 74c65e93..b478ec81 100644 --- a/compose.local.yml +++ b/compose.local.yml @@ -1,119 +1,126 @@ version: "3.8" services: - postgres: - image: postgres:16-alpine - environment: - POSTGRES_USER: ${DB_USER} - POSTGRES_PASSWORD: ${DB_PASSWORD} - POSTGRES_DB: ${DB_NAME} - volumes: - - postgres_data:/var/lib/postgresql/data - networks: - - net - healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${DB_USER} -d ${DB_NAME}"] - interval: 10s - timeout: 5s - retries: 5 - ports: - - "5432:5432" + postgres: + image: postgres:16-alpine + environment: + POSTGRES_USER: ${DB_USER} + POSTGRES_PASSWORD: ${DB_PASSWORD} + POSTGRES_DB: ${DB_NAME} + volumes: + - postgres_data:/var/lib/postgresql/data + networks: + - net + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${DB_USER} -d ${DB_NAME}"] + interval: 10s + timeout: 5s + retries: 5 + ports: + - "5432:5432" - redis: - image: redis:latest - environment: - REDIS_HOST: ${REDIS_HOST} - REDIS_PORT: ${REDIS_PORT} - networks: - - net - ports: - - "6379:6379" - healthcheck: - test: ["CMD", "redis-cli", "ping"] - interval: 30s - retries: 3 - start_period: 10s - timeout: 5s + redis: + image: redis:latest + environment: + REDIS_HOST: ${REDIS_HOST} + REDIS_PORT: ${REDIS_PORT} + networks: + - net + ports: + - "6379:6379" + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 30s + retries: 3 + start_period: 10s + timeout: 5s - backend: - build: - context: . - dockerfile: ./services/backend/Dockerfile.local - image: backend:latest - env_file: - - .env - volumes: - - .env:/app/.env - # 소스 코드 마운트 - - ./apps/backend:/app/apps/backend - - ./apps/frontend:/app/apps/frontend - # 의존성 캐시를 위한 볼륨 - - backend_node_modules:/app/node_modules - - backend_app_node_modules:/app/apps/backend/node_modules - - frontend_app_node_modules:/app/apps/frontend/node_modules - depends_on: - postgres: - condition: service_healthy - redis: - condition: service_healthy - networks: - - net - ports: - - "5173:5173" # Vite dev server - - "3000:3000" # 백엔드 API 포트 + backend: + build: + context: . + dockerfile: ./services/backend/Dockerfile.local + image: backend:latest + env_file: + - .env + volumes: + - .env:/app/.env + - ./apps/backend:/app/apps/backend + - ./apps/frontend:/app/apps/frontend + - root_node_modules:/app/node_modules + - backend_app_node_modules:/app/apps/backend/node_modules + - frontend_app_node_modules:/app/apps/frontend/node_modules + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + networks: + - net + ports: + - "5173:5173" # Vite dev server + - "3000:3000" # 백엔드 API 포트 + entrypoint: | + sh -c " + yarn install && + yarn dev + " - websocket: - build: - context: . - dockerfile: ./services/websocket/Dockerfile.local - image: websocket:latest - env_file: - - .env - volumes: - - .env:/app/.env - # 소스 코드 마운트 - - ./apps/websocket:/app/apps/websocket - # 의존성 캐시를 위한 볼륨 - - websocket_node_modules:/app/node_modules - - websocket_app_node_modules:/app/apps/websocket/node_modules - depends_on: - postgres: - condition: service_healthy - redis: - condition: service_healthy - networks: - - net - ports: - - "4242:4242" # WebSocket 포트 + websocket: + build: + context: . + dockerfile: ./services/websocket/Dockerfile.local + image: websocket:latest + env_file: + - .env + volumes: + - .env:/app/.env + - ./apps/websocket:/app/apps/websocket + - root_node_modules:/app/node_modules + - websocket_app_node_modules:/app/apps/websocket/node_modules + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + networks: + - net + ports: + - "4242:4242" # WebSocket 포트 + entrypoint: | + sh -c " + yarn install && + yarn dev + " - nginx: - build: - context: . - dockerfile: ./services/nginx/Dockerfile.local - ports: - - "80:80" - - "443:443" - depends_on: - - backend - - websocket - networks: - - net - volumes: - - type: bind - source: ./services/nginx/ssl - target: /etc/nginx/ssl - bind: - create_host_path: true - propagation: rprivate - - ./services/nginx/conf.d:/etc/nginx/conf.d + nginx: + build: + context: . + dockerfile: ./services/nginx/Dockerfile.local + ports: + - "80:80" + - "443:443" + depends_on: + - backend + - websocket + networks: + - net + volumes: + - type: bind + source: ./services/nginx/ssl + target: /etc/nginx/ssl + bind: + create_host_path: true + propagation: rprivate + - ./services/nginx/conf.d:/etc/nginx/conf.d networks: - net: + net: volumes: - postgres_data: - backend_node_modules: - backend_app_node_modules: - frontend_app_node_modules: - websocket_node_modules: - websocket_app_node_modules: + postgres_data: + root_node_modules: + backend_node_modules: + backend_app_node_modules: + frontend_app_node_modules: + websocket_node_modules: + websocket_app_node_modules: diff --git a/package.json b/package.json index d625b9e7..df307052 100644 --- a/package.json +++ b/package.json @@ -1,30 +1,30 @@ { - "name": "octodocs", - "version": "1.0.0", - "main": "index.js", - "repository": "https://github.com/boostcampwm-2024/web15-OctoDocs.git", - "author": "ez <105545215+ezcolin2@users.noreply.github.com>", - "license": "MIT", - "scripts": { - "dev": "turbo run dev --parallel", - "build": "turbo run build", - "start": "node apps/backend/dist/main.js", - "start:backend": "node apps/backend/dist/main.js", - "start:websocket": "node apps/websocket/dist/main.js", - "lint": "turbo run lint", - "test": "turbo run test", - "docker:dev": "docker compose -f compose.local.yml up", - "docker:dev:down": "docker compose -f compose.local.yml down", - "docker:dev:clean": "docker compose -v -f compose.local.yml down", - "docker:dev:fclean": "docker compose -v -f compose.local.yml down --rmi all", - "ssl:generate": "cd services/nginx/ssl && bash ./generate-cert.sh" - }, - "dependencies": { - "turbo": "^2.3.0" - }, - "private": true, - "workspaces": [ - "apps/*" - ], - "packageManager": "yarn@1.22.22" + "name": "octodocs", + "version": "1.0.0", + "main": "index.js", + "repository": "https://github.com/boostcampwm-2024/web15-OctoDocs.git", + "author": "ez <105545215+ezcolin2@users.noreply.github.com>", + "license": "MIT", + "scripts": { + "dev": "turbo run dev --parallel", + "build": "turbo run build", + "start": "node apps/backend/dist/main.js", + "start:backend": "node apps/backend/dist/main.js", + "start:websocket": "node apps/websocket/dist/main.js", + "lint": "turbo run lint", + "test": "turbo run test", + "docker:dev": "docker compose -f compose.local.yml up", + "docker:dev:down": "docker compose -f compose.local.yml down", + "docker:dev:clean": "docker compose -v -f compose.local.yml down", + "docker:dev:fclean": "docker compose -v -f compose.local.yml down --rmi all", + "ssl:generate": "cd services/nginx/ssl && bash ./generate-cert.sh" + }, + "dependencies": { + "turbo": "^2.3.0" + }, + "private": true, + "workspaces": [ + "apps/*" + ], + "packageManager": "yarn@1.22.22" } diff --git a/yarn.lock b/yarn.lock index 569729b0..f2fd7dbd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9999,7 +9999,16 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -10031,7 +10040,14 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -10979,7 +10995,7 @@ word-wrap@^1.2.5: resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -10997,6 +11013,15 @@ wrap-ansi@^6.0.1, wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" From 1b3a74ed4ff94da6466392b277e5cccc8d949f64 Mon Sep 17 00:00:00 2001 From: baegyeong Date: Thu, 9 Jan 2025 16:19:54 +0900 Subject: [PATCH 17/26] =?UTF-8?q?refactor:=20editorView=EC=97=90=20lazy=20?= =?UTF-8?q?loading=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - currentPage를 이용한 EditorView 분기 처리 추가 --- apps/frontend/src/app/App.tsx | 18 +++++++++++++----- apps/frontend/src/widgets/EditorView/index.ts | 2 +- .../src/widgets/EditorView/ui/index.tsx | 2 +- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/apps/frontend/src/app/App.tsx b/apps/frontend/src/app/App.tsx index 59ef63f0..5e15b2e7 100644 --- a/apps/frontend/src/app/App.tsx +++ b/apps/frontend/src/app/App.tsx @@ -1,15 +1,19 @@ +import { lazy, Suspense } from "react"; import { useSyncedUsers } from "@/entities/user"; import { useProtectedWorkspace } from "@/features/workspace"; import { CanvasView } from "@/widgets/CanvasView"; -import { EditorView } from "@/widgets/EditorView"; import { NodeToolsView } from "@/widgets/NodeToolsView"; import { PageSideBarView } from "@/widgets/PageSideBarView"; import { CanvasToolsView } from "@/widgets/CanvasToolsView"; -import { SideWrapper } from "@/shared/ui"; +import { EditorSkeleton, SideWrapper } from "@/shared/ui"; +import { usePageStore } from "@/entities/page"; + +const EditorView = lazy(() => import("@/widgets/EditorView")); function App() { useSyncedUsers(); const { isLoading } = useProtectedWorkspace(); + const { currentPage } = usePageStore(); if (isLoading) { return ( @@ -21,9 +25,13 @@ function App() { return (
- - - + {currentPage && ( + + }> + + + + )} Date: Thu, 9 Jan 2025 16:35:37 +0900 Subject: [PATCH 18/26] =?UTF-8?q?fix:=20=EC=8A=A4=EC=BC=88=EB=A0=88?= =?UTF-8?q?=ED=86=A4=20UI=EC=97=90=EC=84=9C=20prop=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=8A=A4=ED=83=80=EC=9D=BC=EC=9D=84=20=EB=B0=9B=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/frontend/src/shared/ui/Skeleton/index.tsx | 8 ++++++-- apps/frontend/src/shared/ui/index.ts | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/apps/frontend/src/shared/ui/Skeleton/index.tsx b/apps/frontend/src/shared/ui/Skeleton/index.tsx index a35bab61..efe24f36 100644 --- a/apps/frontend/src/shared/ui/Skeleton/index.tsx +++ b/apps/frontend/src/shared/ui/Skeleton/index.tsx @@ -1,6 +1,10 @@ -export function EditorSkeleton() { +interface SkeletonProps { + className: string; +} + +export function Skeleton({ className }: SkeletonProps) { return ( -
+
); diff --git a/apps/frontend/src/shared/ui/index.ts b/apps/frontend/src/shared/ui/index.ts index b41f2a23..71bc2fc2 100644 --- a/apps/frontend/src/shared/ui/index.ts +++ b/apps/frontend/src/shared/ui/index.ts @@ -11,4 +11,4 @@ export { Popover } from "./Popover"; export { ScrollWrapper } from "./ScrollWrapper"; export { SideWrapper } from "./SideWrapper"; export { Switch } from "./Switch"; -export { EditorSkeleton } from "./Skeleton"; +export { Skeleton } from "./Skeleton"; From 5fa62926cc39d6d891d75529442f829b29d25370 Mon Sep 17 00:00:00 2001 From: baegyeong Date: Thu, 9 Jan 2025 16:36:06 +0900 Subject: [PATCH 19/26] =?UTF-8?q?refactor:=20=EC=8A=A4=EC=BC=88=EB=A0=88?= =?UTF-8?q?=ED=86=A4=20UI=EB=B6=88=EB=9F=AC=EC=98=AC=20=EB=95=8C=20?= =?UTF-8?q?=ED=81=AC=EA=B8=B0=EB=A5=BC=20=EC=A7=80=EC=A0=95=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/frontend/src/app/App.tsx | 8 ++++++-- .../features/workspace/ui/ShareTool/index.tsx | 16 +++++++++++++--- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/apps/frontend/src/app/App.tsx b/apps/frontend/src/app/App.tsx index 5e15b2e7..030ecd71 100644 --- a/apps/frontend/src/app/App.tsx +++ b/apps/frontend/src/app/App.tsx @@ -5,7 +5,7 @@ import { CanvasView } from "@/widgets/CanvasView"; import { NodeToolsView } from "@/widgets/NodeToolsView"; import { PageSideBarView } from "@/widgets/PageSideBarView"; import { CanvasToolsView } from "@/widgets/CanvasToolsView"; -import { EditorSkeleton, SideWrapper } from "@/shared/ui"; +import { SideWrapper, Skeleton } from "@/shared/ui"; import { usePageStore } from "@/entities/page"; const EditorView = lazy(() => import("@/widgets/EditorView")); @@ -27,7 +27,11 @@ function App() {
{currentPage && ( - }> + + } + > diff --git a/apps/frontend/src/features/workspace/ui/ShareTool/index.tsx b/apps/frontend/src/features/workspace/ui/ShareTool/index.tsx index 77366ec3..3271762c 100644 --- a/apps/frontend/src/features/workspace/ui/ShareTool/index.tsx +++ b/apps/frontend/src/features/workspace/ui/ShareTool/index.tsx @@ -1,6 +1,8 @@ +import { lazy, Suspense } from "react"; import { Sharebutton } from "./ShareButton"; -import { SharePanel } from "./SharePanel"; -import { Popover } from "@/shared/ui"; +import { Popover, Skeleton } from "@/shared/ui"; + +const SharePanel = lazy(() => delayForDemo(import("./SharePanel"))); export function ShareTool() { return ( @@ -10,9 +12,17 @@ export function ShareTool() { - + }> + +
); } + +function delayForDemo(promise) { + return new Promise((resolve) => { + setTimeout(resolve, 2000); + }).then(() => promise); +} From 47cbcb7254d6a745de5608d20160f325be1558f1 Mon Sep 17 00:00:00 2001 From: pkh0106 Date: Thu, 9 Jan 2025 16:40:38 +0900 Subject: [PATCH 20/26] =?UTF-8?q?fix:=20=EC=8A=A4=EC=BC=88=EB=A0=88?= =?UTF-8?q?=ED=86=A4=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EB=82=B4?= =?UTF-8?q?=EC=9D=98=20padding=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/features/workspace/ui/ShareTool/SharePanel.tsx | 2 +- .../src/features/workspace/ui/ShareTool/index.tsx | 10 ++-------- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/apps/frontend/src/features/workspace/ui/ShareTool/SharePanel.tsx b/apps/frontend/src/features/workspace/ui/ShareTool/SharePanel.tsx index 07de6fd2..aa515dd0 100644 --- a/apps/frontend/src/features/workspace/ui/ShareTool/SharePanel.tsx +++ b/apps/frontend/src/features/workspace/ui/ShareTool/SharePanel.tsx @@ -75,7 +75,7 @@ export function SharePanel() { const isDisabled = isGuest || isPending; return ( -
+
공개 범위
diff --git a/apps/frontend/src/features/workspace/ui/ShareTool/index.tsx b/apps/frontend/src/features/workspace/ui/ShareTool/index.tsx index 3271762c..7d4fe5f7 100644 --- a/apps/frontend/src/features/workspace/ui/ShareTool/index.tsx +++ b/apps/frontend/src/features/workspace/ui/ShareTool/index.tsx @@ -2,7 +2,7 @@ import { lazy, Suspense } from "react"; import { Sharebutton } from "./ShareButton"; import { Popover, Skeleton } from "@/shared/ui"; -const SharePanel = lazy(() => delayForDemo(import("./SharePanel"))); +const SharePanel = lazy(() => import("./SharePanel")); export function ShareTool() { return ( @@ -11,7 +11,7 @@ export function ShareTool() { - + }> @@ -20,9 +20,3 @@ export function ShareTool() {
); } - -function delayForDemo(promise) { - return new Promise((resolve) => { - setTimeout(resolve, 2000); - }).then(() => promise); -} From 972f836399f4e166d5390eca48c8629cfcbe377b Mon Sep 17 00:00:00 2001 From: pkh0106 Date: Thu, 9 Jan 2025 16:59:51 +0900 Subject: [PATCH 21/26] =?UTF-8?q?refactor:=20=EC=9B=8C=ED=81=AC=EC=8A=A4?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=8A=A4=20=ED=8C=A8=EB=84=90=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC=20=EB=B0=8F=20lazy=20loading=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../workspace/ui/WorkspacePanel/index.tsx | 44 ++++++++++++++++ .../src/widgets/UserInfoView/ui/index.tsx | 51 ++++--------------- 2 files changed, 55 insertions(+), 40 deletions(-) create mode 100644 apps/frontend/src/features/workspace/ui/WorkspacePanel/index.tsx diff --git a/apps/frontend/src/features/workspace/ui/WorkspacePanel/index.tsx b/apps/frontend/src/features/workspace/ui/WorkspacePanel/index.tsx new file mode 100644 index 00000000..67d89d18 --- /dev/null +++ b/apps/frontend/src/features/workspace/ui/WorkspacePanel/index.tsx @@ -0,0 +1,44 @@ +import { useState } from "react"; +import { UserProfile } from "@/entities/user"; +import { Logout, useGetUser, LoginForm } from "@/features/auth"; +import { + WorkspaceAddButton, + WorkspaceForm, + WorkspaceList, +} from "@/features/workspace"; +import { Divider } from "@/shared/ui"; + +export default function WorkspacePanel() { + const { data } = useGetUser(); + const [isModalOpen, setIsModalOpen] = useState(false); + + const onOpenModal = () => { + setIsModalOpen(true); + }; + + const onCloseModal = () => { + setIsModalOpen(false); + }; + return ( +
+ {data ? ( +
+ + + + + +
+ + +
+
+ ) : ( + + )} +
+ ); +} diff --git a/apps/frontend/src/widgets/UserInfoView/ui/index.tsx b/apps/frontend/src/widgets/UserInfoView/ui/index.tsx index e0d1c0ee..4b6c02ed 100644 --- a/apps/frontend/src/widgets/UserInfoView/ui/index.tsx +++ b/apps/frontend/src/widgets/UserInfoView/ui/index.tsx @@ -1,52 +1,23 @@ -import { useState } from "react"; - -import { UserProfile } from "@/entities/user"; -import { LoginForm, Logout, useGetUser } from "@/features/auth"; +import { lazy } from "react"; import { LogoBtn } from "@/features/pageSidebar"; -import { - WorkspaceAddButton, - WorkspaceForm, - WorkspaceList, -} from "@/features/workspace"; -import { Divider, Popover } from "@/shared/ui"; - -export function UserInfoView() { - const { data } = useGetUser(); - const [isModalOpen, setIsModalOpen] = useState(false); +import { Popover, Skeleton } from "@/shared/ui"; +import { Suspense } from "react"; - const onOpenModal = () => { - setIsModalOpen(true); - }; - - const onCloseModal = () => { - setIsModalOpen(false); - }; +const WorkspacePanel = lazy( + () => import("@/features/workspace/ui/WorkspacePanel"), +); +export function UserInfoView() { return (
- - {data ? ( -
- - - - - -
- - -
-
- ) : ( - - )} + + }> + +
From 77ba38fb6a10b752087dd469be9593f83071932f Mon Sep 17 00:00:00 2001 From: pkh0106 Date: Thu, 9 Jan 2025 17:43:18 +0900 Subject: [PATCH 22/26] =?UTF-8?q?refactor:=20=ED=8E=98=EC=9D=B4=EC=A7=80?= =?UTF-8?q?=20=EB=AA=A9=EB=A1=9D=20=ED=8C=A8=EB=84=90=20=EB=B6=84=EB=A6=AC?= =?UTF-8?q?=20=EB=B0=8F=20lazy=20loading=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: baegyeong --- .../src/features/pageSidebar/index.ts | 1 + .../features/pageSidebar/model/useNoteList.ts | 12 +++++- .../pageSidebar/ui/NoteList/index.tsx | 41 +++++++++---------- .../pageSidebar/ui/PageListPanel/index.tsx | 15 +++++++ .../src/widgets/PageSideBarView/ui/index.tsx | 24 ++++++----- 5 files changed, 59 insertions(+), 34 deletions(-) create mode 100644 apps/frontend/src/features/pageSidebar/ui/PageListPanel/index.tsx diff --git a/apps/frontend/src/features/pageSidebar/index.ts b/apps/frontend/src/features/pageSidebar/index.ts index fd8d9168..aca4b26f 100644 --- a/apps/frontend/src/features/pageSidebar/index.ts +++ b/apps/frontend/src/features/pageSidebar/index.ts @@ -2,3 +2,4 @@ export { LogoBtn } from "./ui/LogoBtn"; export { NoteList } from "./ui/NoteList"; export { Tools } from "./ui/Tools"; export { WorkspaceNav } from "./ui/WorkspaceNav"; +export { default } from "./ui/PageListPanel"; diff --git a/apps/frontend/src/features/pageSidebar/model/useNoteList.ts b/apps/frontend/src/features/pageSidebar/model/useNoteList.ts index 93e626b7..44e206e6 100644 --- a/apps/frontend/src/features/pageSidebar/model/useNoteList.ts +++ b/apps/frontend/src/features/pageSidebar/model/useNoteList.ts @@ -14,11 +14,21 @@ export const useNoteList = () => { if (!canvas.provider) return; const nodesMap = canvas.provider.doc.getMap("nodes"); - nodesMap.observe(() => { + const initializePages = () => { const yNodes = Array.from(nodesMap.values()) as Node[]; const data = yNodes.map((yNode) => yNode.data) as NoteNodeData[]; setPages(data); + }; + + initializePages(); + + nodesMap.observe(() => { + initializePages(); }); + + return () => { + nodesMap.unobserve(initializePages); + }; }, [canvas.provider]); const [noteIdToDelete, setNoteIdToDelete] = useState(null); diff --git a/apps/frontend/src/features/pageSidebar/ui/NoteList/index.tsx b/apps/frontend/src/features/pageSidebar/ui/NoteList/index.tsx index dcfaba9d..4032957f 100644 --- a/apps/frontend/src/features/pageSidebar/ui/NoteList/index.tsx +++ b/apps/frontend/src/features/pageSidebar/ui/NoteList/index.tsx @@ -19,10 +19,6 @@ export function NoteList({ className }: NoteListProps) { onCloseModal, } = useNoteList(); - if (!pages) { - return
로딩중
; - } - return (
- {pages.map(({ id, title, emoji }) => ( - - ))} + +
{title}
+ { + e.stopPropagation(); + openModal(id); + }} + > + + + + ))}
); } diff --git a/apps/frontend/src/features/pageSidebar/ui/PageListPanel/index.tsx b/apps/frontend/src/features/pageSidebar/ui/PageListPanel/index.tsx new file mode 100644 index 00000000..19f44fa4 --- /dev/null +++ b/apps/frontend/src/features/pageSidebar/ui/PageListPanel/index.tsx @@ -0,0 +1,15 @@ +import { NoteList, Tools } from "@/features/pageSidebar"; +import { ScrollWrapper } from "@/shared/ui"; + +export default function PageListPanel() { + return ( +
+
+ +
+ + + +
+ ); +} diff --git a/apps/frontend/src/widgets/PageSideBarView/ui/index.tsx b/apps/frontend/src/widgets/PageSideBarView/ui/index.tsx index a67cc12e..b28d9eff 100644 --- a/apps/frontend/src/widgets/PageSideBarView/ui/index.tsx +++ b/apps/frontend/src/widgets/PageSideBarView/ui/index.tsx @@ -1,8 +1,11 @@ -import { useState } from "react"; +import { useState, lazy, Suspense } from "react"; -import { NoteList, Tools } from "@/features/pageSidebar"; import { TopNavView } from "@/widgets/TopNavView"; -import { ScrollWrapper } from "@/shared/ui"; +import { Skeleton } from "@/shared/ui"; + +const PageListPanel = lazy( + () => import("@/features/pageSidebar/ui/PageListPanel"), +); export function PageSideBarView() { const [isExpanded, setIsExpanded] = useState(false); @@ -16,14 +19,13 @@ export function PageSideBarView() {
-
-
- -
- - - -
+ {isExpanded && ( + } + > + + + )}
); } From 0d9a0b39756485ac3c1f6edde62efbf32e644366 Mon Sep 17 00:00:00 2001 From: pkh0106 Date: Thu, 9 Jan 2025 17:49:02 +0900 Subject: [PATCH 23/26] =?UTF-8?q?refactor:=20=EC=82=AC=EC=9A=A9=EC=9E=90?= =?UTF-8?q?=20=EC=A0=95=EB=B3=B4=20=EA=B4=80=EB=A6=AC=20=ED=8C=A8=EB=84=90?= =?UTF-8?q?=20lazy=20loading=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: baegyeong --- .../src/features/canvasTools/index.ts | 2 +- .../canvasTools/ui/ProfilePanel/index.tsx | 4 ++-- .../src/widgets/CanvasToolsView/ui/index.tsx | 24 +++++++++++-------- 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/apps/frontend/src/features/canvasTools/index.ts b/apps/frontend/src/features/canvasTools/index.ts index 4091cf3d..3edd219c 100644 --- a/apps/frontend/src/features/canvasTools/index.ts +++ b/apps/frontend/src/features/canvasTools/index.ts @@ -1,3 +1,3 @@ export { CursorButton } from "./ui/CursorButton"; export { NodePanel } from "./ui/NodePanel"; -export { ProfilePanel } from "./ui/ProfilePanel"; +export { default } from "./ui/ProfilePanel"; diff --git a/apps/frontend/src/features/canvasTools/ui/ProfilePanel/index.tsx b/apps/frontend/src/features/canvasTools/ui/ProfilePanel/index.tsx index cd8ab1a1..2917646a 100644 --- a/apps/frontend/src/features/canvasTools/ui/ProfilePanel/index.tsx +++ b/apps/frontend/src/features/canvasTools/ui/ProfilePanel/index.tsx @@ -9,7 +9,7 @@ interface ProfilePanelProps { onClientIdChange: (clientId: string) => void; } -export function ProfilePanel({ +export default function ProfilePanel({ color, clientId, onColorChange, @@ -22,7 +22,7 @@ export function ProfilePanel({ }; return ( -
+
import("@/features/canvasTools")); export function CanvasToolsView() { const { currentUser } = useUserStore(); @@ -22,13 +24,15 @@ export function CanvasToolsView() { - - + + }> + +
From 374837f80eb7dff8b1d01e7a6bbc107c476994f1 Mon Sep 17 00:00:00 2001 From: pkh0106 Date: Thu, 9 Jan 2025 18:01:29 +0900 Subject: [PATCH 24/26] =?UTF-8?q?fix:=20cn=EC=9D=84=20=EC=9D=B4=EC=9A=A9?= =?UTF-8?q?=ED=95=98=EC=97=AC=20Skeleton=20=EC=BB=B4=ED=8F=AC=EB=84=8C?= =?UTF-8?q?=ED=8A=B8=20=EB=82=B4=20div=20=ED=86=B5=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - rounded 적용 안됨 이슈 해결결 Co-authored-by: baegyeong --- apps/frontend/src/shared/ui/Skeleton/index.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/apps/frontend/src/shared/ui/Skeleton/index.tsx b/apps/frontend/src/shared/ui/Skeleton/index.tsx index efe24f36..eb54a9f3 100644 --- a/apps/frontend/src/shared/ui/Skeleton/index.tsx +++ b/apps/frontend/src/shared/ui/Skeleton/index.tsx @@ -1,11 +1,16 @@ +import { cn } from "@/shared/lib"; + interface SkeletonProps { className: string; } export function Skeleton({ className }: SkeletonProps) { return ( -
-
-
+
); } From 3fa0411c83e932f3a5b2669a170895aeaa4f65c9 Mon Sep 17 00:00:00 2001 From: pkh0106 Date: Thu, 9 Jan 2025 18:01:58 +0900 Subject: [PATCH 25/26] =?UTF-8?q?refactor:=20=EB=85=B8=EB=93=9C=20?= =?UTF-8?q?=EC=83=89=EC=83=81=20=EA=B4=80=EB=A6=AC=20=ED=8C=A8=EB=84=90=20?= =?UTF-8?q?lazy=20loading=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: baegyeong --- apps/frontend/src/app/App.tsx | 10 ++++++++-- apps/frontend/src/widgets/NodeToolsView/index.ts | 2 +- apps/frontend/src/widgets/NodeToolsView/ui/index.tsx | 2 +- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/apps/frontend/src/app/App.tsx b/apps/frontend/src/app/App.tsx index 030ecd71..ad0c79e7 100644 --- a/apps/frontend/src/app/App.tsx +++ b/apps/frontend/src/app/App.tsx @@ -2,13 +2,13 @@ import { lazy, Suspense } from "react"; import { useSyncedUsers } from "@/entities/user"; import { useProtectedWorkspace } from "@/features/workspace"; import { CanvasView } from "@/widgets/CanvasView"; -import { NodeToolsView } from "@/widgets/NodeToolsView"; import { PageSideBarView } from "@/widgets/PageSideBarView"; import { CanvasToolsView } from "@/widgets/CanvasToolsView"; import { SideWrapper, Skeleton } from "@/shared/ui"; import { usePageStore } from "@/entities/page"; const EditorView = lazy(() => import("@/widgets/EditorView")); +const NodeToolsView = lazy(() => import("@/widgets/NodeToolsView")); function App() { useSyncedUsers(); @@ -43,7 +43,13 @@ function App() { > - + {currentPage && ( + } + > + + + )}
); diff --git a/apps/frontend/src/widgets/NodeToolsView/index.ts b/apps/frontend/src/widgets/NodeToolsView/index.ts index a79544e0..ac3e2e48 100644 --- a/apps/frontend/src/widgets/NodeToolsView/index.ts +++ b/apps/frontend/src/widgets/NodeToolsView/index.ts @@ -1 +1 @@ -export { NodeToolsView } from "./ui"; +export { default } from "./ui"; diff --git a/apps/frontend/src/widgets/NodeToolsView/ui/index.tsx b/apps/frontend/src/widgets/NodeToolsView/ui/index.tsx index ef88e946..74c1d237 100644 --- a/apps/frontend/src/widgets/NodeToolsView/ui/index.tsx +++ b/apps/frontend/src/widgets/NodeToolsView/ui/index.tsx @@ -1,7 +1,7 @@ import { usePageStore } from "@/entities/page"; import { NodePanel } from "@/features/canvasTools/ui/NodePanel"; -export function NodeToolsView() { +export default function NodeToolsView() { const { currentPage } = usePageStore(); if (!currentPage) return null; From 94414c07e5bc01e5601a51ff075c019bad1339ff Mon Sep 17 00:00:00 2001 From: ez <105545215+ezcolin2@users.noreply.github.com> Date: Thu, 9 Jan 2025 23:16:07 +0900 Subject: [PATCH 26/26] =?UTF-8?q?refactor:=20red=20lock=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0=20=ED=9B=84=20redis=20=EB=82=99=EA=B4=80=EC=A0=81=20?= =?UTF-8?q?=EB=9D=BD=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/backend/src/app.module.ts | 2 - apps/backend/src/page/page.module.ts | 2 - apps/backend/src/page/page.service.ts | 57 +++++----------- apps/backend/src/red-lock/red-lock.module.ts | 27 -------- apps/backend/src/redis/redis.module.ts | 5 +- apps/backend/src/redis/redis.service.ts | 26 +------- apps/backend/src/tasks/tasks.service.ts | 8 ++- .../websocket/src/red-lock/red-lock.module.ts | 27 -------- apps/websocket/src/redis/redis.module.ts | 5 +- apps/websocket/src/redis/redis.service.ts | 43 ++++-------- redis-test.js | 66 +++++++++++++++++++ 11 files changed, 109 insertions(+), 159 deletions(-) delete mode 100644 apps/backend/src/red-lock/red-lock.module.ts delete mode 100644 apps/websocket/src/red-lock/red-lock.module.ts create mode 100644 redis-test.js diff --git a/apps/backend/src/app.module.ts b/apps/backend/src/app.module.ts index 720b1d37..28503463 100644 --- a/apps/backend/src/app.module.ts +++ b/apps/backend/src/app.module.ts @@ -20,7 +20,6 @@ import { WorkspaceModule } from './workspace/workspace.module'; import { RoleModule } from './role/role.module'; import { TasksModule } from './tasks/tasks.module'; import { ScheduleModule } from '@nestjs/schedule'; -import { RedLockModule } from './red-lock/red-lock.module'; @Module({ imports: [ @@ -54,7 +53,6 @@ import { RedLockModule } from './red-lock/red-lock.module'; WorkspaceModule, RoleModule, TasksModule, - RedLockModule, ], controllers: [AppController], providers: [AppService], diff --git a/apps/backend/src/page/page.module.ts b/apps/backend/src/page/page.module.ts index 74d30922..743427e3 100644 --- a/apps/backend/src/page/page.module.ts +++ b/apps/backend/src/page/page.module.ts @@ -6,14 +6,12 @@ import { Page } from './page.entity'; import { PageRepository } from './page.repository'; import { NodeModule } from '../node/node.module'; import { WorkspaceModule } from '../workspace/workspace.module'; -import { RedLockModule } from '../red-lock/red-lock.module'; @Module({ imports: [ TypeOrmModule.forFeature([Page]), forwardRef(() => NodeModule), WorkspaceModule, - RedLockModule, ], controllers: [PageController], providers: [PageService, PageRepository], diff --git a/apps/backend/src/page/page.service.ts b/apps/backend/src/page/page.service.ts index 6fd5eab6..d1568825 100644 --- a/apps/backend/src/page/page.service.ts +++ b/apps/backend/src/page/page.service.ts @@ -8,7 +8,6 @@ import { UpdatePageDto } from './dtos/updatePage.dto'; import { UpdatePartialPageDto } from './dtos/updatePartialPage.dto'; import { PageNotFoundException } from '../exception/page.exception'; import { WorkspaceNotFoundException } from '../exception/workspace.exception'; -import Redlock from 'redlock'; const RED_LOCK_TOKEN = 'RED_LOCK'; @@ -18,7 +17,6 @@ export class PageService { private readonly pageRepository: PageRepository, private readonly nodeRepository: NodeRepository, private readonly workspaceRepository: WorkspaceRepository, - @Inject(RED_LOCK_TOKEN) private readonly redisLock: Redlock, ) {} /** * redis에 저장된 페이지 정보를 다음 과정을 통해 주기적으로 데이터베이스에 반영한다. @@ -26,12 +24,6 @@ export class PageService { * 1. redis에서 해당 페이지의 title과 content를 가져온다. * 2. 데이터베이스에 해당 페이지의 title과 content를 갱신한다. * 3. redis에서 해당 페이지 정보를 삭제한다. - * - * 만약 1번 과정을 진행한 상태에서 page가 삭제된다면 오류가 발생한다. - * 위 과정을 진행하는 동안 page 정보 수정을 막기 위해 lock을 사용한다. - * - * 동기화를 위해 기존 페이지에 접근하여 수정하는 로직은 RedLock 알고리즘을 통해 락을 획득할 수 있을 때만 수행한다. - * 기존 페이지에 접근하여 연산하는 로직의 경우 RedLock 알고리즘을 사용하여 동시 접근을 방지한다. */ async createPage(dto: CreatePageDto): Promise { const { title, content, workspaceId, x, y, emoji } = dto; @@ -62,42 +54,29 @@ export class PageService { } async deletePage(id: number): Promise { - // 락을 획득할 때까지 기다린다. - const lock = await this.redisLock.acquire([`user:${id.toString()}`], 1000); - try { - // 페이지를 삭제한다. - const deleteResult = await this.pageRepository.delete(id); - - // 만약 삭제된 페이지가 없으면 페이지를 찾지 못한 것 - if (!deleteResult.affected) { - throw new PageNotFoundException(); - } - } finally { - // 락을 해제한다. - await lock.release(); + // 페이지를 삭제한다. + const deleteResult = await this.pageRepository.delete(id); + + // 만약 삭제된 페이지가 없으면 페이지를 찾지 못한 것 + if (!deleteResult.affected) { + throw new PageNotFoundException(); } } async updatePage(id: number, dto: UpdatePageDto): Promise { - // 락을 획득할 때까지 기다린다. - const lock = await this.redisLock.acquire([`user:${id.toString()}`], 1000); - try { - // 갱신할 페이지를 조회한다. - // 페이지를 조회한다. - const page = await this.pageRepository.findOneBy({ id }); - - // 페이지가 없으면 NotFound 에러 - if (!page) { - throw new PageNotFoundException(); - } - // 페이지 정보를 갱신한다. - const newPage = Object.assign({}, page, dto); - - // 변경된 페이지를 저장 - return await this.pageRepository.save(newPage); - } finally { - await lock.release(); + // 갱신할 페이지를 조회한다. + // 페이지를 조회한다. + const page = await this.pageRepository.findOneBy({ id }); + + // 페이지가 없으면 NotFound 에러 + if (!page) { + throw new PageNotFoundException(); } + // 페이지 정보를 갱신한다. + const newPage = Object.assign({}, page, dto); + + // 변경된 페이지를 저장 + return await this.pageRepository.save(newPage); } async updateBulkPage(pages: UpdatePartialPageDto[]) { diff --git a/apps/backend/src/red-lock/red-lock.module.ts b/apps/backend/src/red-lock/red-lock.module.ts deleted file mode 100644 index d2d9f574..00000000 --- a/apps/backend/src/red-lock/red-lock.module.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Module, forwardRef } from '@nestjs/common'; -import Redis from 'ioredis'; -import Redlock from 'redlock'; -import { RedisModule } from '../redis/redis.module'; -const RED_LOCK_TOKEN = 'RED_LOCK'; -const REDIS_CLIENT_TOKEN = 'REDIS_CLIENT'; - -@Module({ - imports: [forwardRef(() => RedisModule)], - providers: [ - { - provide: RED_LOCK_TOKEN, - useFactory: (redisClient: Redis) => { - return new Redlock([redisClient], { - driftFactor: 0.01, - retryCount: 10, - retryDelay: 200, - retryJitter: 200, - automaticExtensionThreshold: 500, - }); - }, - inject: [REDIS_CLIENT_TOKEN], - }, - ], - exports: [RED_LOCK_TOKEN], -}) -export class RedLockModule {} diff --git a/apps/backend/src/redis/redis.module.ts b/apps/backend/src/redis/redis.module.ts index 152d05b3..666cd746 100644 --- a/apps/backend/src/redis/redis.module.ts +++ b/apps/backend/src/redis/redis.module.ts @@ -1,14 +1,13 @@ -import { Module, forwardRef } from '@nestjs/common'; +import { Module } from '@nestjs/common'; import { ConfigModule, ConfigService } from '@nestjs/config'; import { RedisService } from './redis.service'; import Redis from 'ioredis'; -import { RedLockModule } from '../red-lock/red-lock.module'; // 의존성 주입할 때 redis client를 식별할 토큰 const REDIS_CLIENT_TOKEN = 'REDIS_CLIENT'; @Module({ - imports: [ConfigModule, forwardRef(() => RedLockModule)], // ConfigModule 추가 + imports: [ConfigModule], // ConfigModule 추가 providers: [ RedisService, { diff --git a/apps/backend/src/redis/redis.service.ts b/apps/backend/src/redis/redis.service.ts index 57341b9a..27f3431d 100644 --- a/apps/backend/src/redis/redis.service.ts +++ b/apps/backend/src/redis/redis.service.ts @@ -1,7 +1,6 @@ import { Injectable } from '@nestjs/common'; import { Inject } from '@nestjs/common'; import Redis from 'ioredis'; -import Redlock from 'redlock'; const REDIS_CLIENT_TOKEN = 'REDIS_CLIENT'; const RED_LOCK_TOKEN = 'RED_LOCK'; @@ -27,7 +26,6 @@ export type RedisEdge = { export class RedisService { constructor( @Inject(REDIS_CLIENT_TOKEN) private readonly redisClient: Redis, - @Inject(RED_LOCK_TOKEN) private readonly redisLock: Redlock, ) {} async getAllKeys(pattern) { @@ -46,32 +44,14 @@ export class RedisService { } async set(key: string, value: object) { - // 락을 획득할 때까지 기다린다. - const lock = await this.redisLock.acquire([`user:${key}`], 1000); - try { - await this.redisClient.hset(key, Object.entries(value)); - } finally { - lock.release(); - } + await this.redisClient.hset(key, Object.entries(value)); } async setField(key: string, field: string, value: string) { - // 락을 획득할 때까지 기다린다. - const lock = await this.redisLock.acquire([`user:${key}`], 1000); - try { - return await this.redisClient.hset(key, field, value); - } finally { - lock.release(); - } + return await this.redisClient.hset(key, field, value); } async delete(key: string) { - // 락을 획득할 때까지 기다린다. - const lock = await this.redisLock.acquire([`user:${key}`], 1000); - try { - return await this.redisClient.del(key); - } finally { - lock.release(); - } + return await this.redisClient.del(key); } } diff --git a/apps/backend/src/tasks/tasks.service.ts b/apps/backend/src/tasks/tasks.service.ts index 11e20d71..c9deabee 100644 --- a/apps/backend/src/tasks/tasks.service.ts +++ b/apps/backend/src/tasks/tasks.service.ts @@ -24,7 +24,7 @@ export class TasksService { @InjectDataSource() private readonly dataSource: DataSource, ) {} - @Cron(CronExpression.EVERY_10_SECONDS) + @Cron(CronExpression.EVERY_30_SECONDS) async handleCron() { this.logger.log('스케줄러 시작'); // 시작 시간 @@ -56,6 +56,8 @@ export class TasksService { } async migratePage(key: string) { + // 낙관적 락 적용 + await this.redisClient.watch(key); const data = await this.redisClient.hgetall(key); const redisData = Object.fromEntries( Object.entries(data).map(([field, value]) => [field, value]), @@ -110,6 +112,8 @@ export class TasksService { } async migrateNode(key: string) { + // 낙관적 락 적용 + await this.redisClient.watch(key); const data = await this.redisClient.hgetall(key); const redisData = Object.fromEntries( Object.entries(data).map(([field, value]) => [field, value]), @@ -163,6 +167,8 @@ export class TasksService { } async migrateEdge(key: string) { + // 낙관적 락 적용 + await this.redisClient.watch(key); const data = await this.redisClient.hgetall(key); const redisData = Object.fromEntries( Object.entries(data).map(([field, value]) => [field, value]), diff --git a/apps/websocket/src/red-lock/red-lock.module.ts b/apps/websocket/src/red-lock/red-lock.module.ts deleted file mode 100644 index d2d9f574..00000000 --- a/apps/websocket/src/red-lock/red-lock.module.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Module, forwardRef } from '@nestjs/common'; -import Redis from 'ioredis'; -import Redlock from 'redlock'; -import { RedisModule } from '../redis/redis.module'; -const RED_LOCK_TOKEN = 'RED_LOCK'; -const REDIS_CLIENT_TOKEN = 'REDIS_CLIENT'; - -@Module({ - imports: [forwardRef(() => RedisModule)], - providers: [ - { - provide: RED_LOCK_TOKEN, - useFactory: (redisClient: Redis) => { - return new Redlock([redisClient], { - driftFactor: 0.01, - retryCount: 10, - retryDelay: 200, - retryJitter: 200, - automaticExtensionThreshold: 500, - }); - }, - inject: [REDIS_CLIENT_TOKEN], - }, - ], - exports: [RED_LOCK_TOKEN], -}) -export class RedLockModule {} diff --git a/apps/websocket/src/redis/redis.module.ts b/apps/websocket/src/redis/redis.module.ts index 152d05b3..666cd746 100644 --- a/apps/websocket/src/redis/redis.module.ts +++ b/apps/websocket/src/redis/redis.module.ts @@ -1,14 +1,13 @@ -import { Module, forwardRef } from '@nestjs/common'; +import { Module } from '@nestjs/common'; import { ConfigModule, ConfigService } from '@nestjs/config'; import { RedisService } from './redis.service'; import Redis from 'ioredis'; -import { RedLockModule } from '../red-lock/red-lock.module'; // 의존성 주입할 때 redis client를 식별할 토큰 const REDIS_CLIENT_TOKEN = 'REDIS_CLIENT'; @Module({ - imports: [ConfigModule, forwardRef(() => RedLockModule)], // ConfigModule 추가 + imports: [ConfigModule], // ConfigModule 추가 providers: [ RedisService, { diff --git a/apps/websocket/src/redis/redis.service.ts b/apps/websocket/src/redis/redis.service.ts index 718d5330..a5b8b03c 100644 --- a/apps/websocket/src/redis/redis.service.ts +++ b/apps/websocket/src/redis/redis.service.ts @@ -1,9 +1,7 @@ import { Injectable } from '@nestjs/common'; import { Inject } from '@nestjs/common'; import Redis from 'ioredis'; -import Redlock from 'redlock'; const REDIS_CLIENT_TOKEN = 'REDIS_CLIENT'; -const RED_LOCK_TOKEN = 'RED_LOCK'; type RedisPage = { title?: string; @@ -14,7 +12,6 @@ type RedisPage = { export class RedisService { constructor( @Inject(REDIS_CLIENT_TOKEN) private readonly redisClient: Redis, - @Inject(RED_LOCK_TOKEN) private readonly redisLock: Redlock, ) {} createStream() { @@ -29,40 +26,22 @@ export class RedisService { } async set(key: string, value: object) { - // 락을 획득할 때까지 기다린다. - const lock = await this.redisLock.acquire([`user:${key}`], 1000); - try { - await this.redisClient.hset(key, Object.entries(value)); - } finally { - lock.release(); - } + await this.redisClient.hset(key, Object.entries(value)); } async setFields(key: string, map: Record) { - // 락을 획득할 때까지 기다린다. - const lock = await this.redisLock.acquire([`user:${key}`], 1000); - try { - // return await this.redisClient.hset(key, ); - // fieldValueArr 배열을 평탄화하여 [field, value, field, value, ...] 형태로 변환 - const flattenedFields = Object.entries(map).flatMap(([field, value]) => [ - field, - value, - ]); - - // hset을 통해 한 번에 여러 필드를 설정 - return await this.redisClient.hset(key, ...flattenedFields); - } finally { - lock.release(); - } + // return await this.redisClient.hset(key, ); + // fieldValueArr 배열을 평탄화하여 [field, value, field, value, ...] 형태로 변환 + const flattenedFields = Object.entries(map).flatMap(([field, value]) => [ + field, + value, + ]); + + // hset을 통해 한 번에 여러 필드를 설정 + return await this.redisClient.hset(key, ...flattenedFields); } async delete(key: string) { - // 락을 획득할 때까지 기다린다. - const lock = await this.redisLock.acquire([`user:${key}`], 1000); - try { - return await this.redisClient.del(key); - } finally { - lock.release(); - } + return await this.redisClient.del(key); } } diff --git a/redis-test.js b/redis-test.js new file mode 100644 index 00000000..029c0edc --- /dev/null +++ b/redis-test.js @@ -0,0 +1,66 @@ +const Redis = require("ioredis"); + +// Redis 클라이언트 생성 +const redis = new Redis({ + host: "localhost", + port: 6379, +}); + +// 여러 개의 node와 page 정보를 삽입하는 함수 +async function insertData() { + const nodes = []; + const pages = []; + + // node:1부터 node:100까지 생성 + for (let i = 1; i <= 500; i++) { + const x = 180; + const y = 479; + const color = "#FFFFFF"; + + nodes.push({ + id: i, + x: x, + y: y, + color: color, + }); + + const content = `{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"페이지 내용 ${i}"}]}]}`; + const title = `페이지 제목 ${i}`; + + pages.push({ + id: i, + content: content, + title: title, + }); + } + + // node 정보 삽입 + for (const node of nodes) { + await redis.hmset( + `node:${node.id}`, + "x", + node.x, + "y", + node.y, + "color", + node.color + ); + console.log(`Inserted node:${node.id}`); + } + + // page 정보 삽입 + for (const page of pages) { + await redis.hmset( + `page:${page.id}`, + "content", + page.content, + "title", + page.title + ); + console.log(`Inserted page:${page.id}`); + } + + console.log("Data insertion complete"); +} + +insertData();