Skip to content

[FE] 동시 편집을 위한 마인드맵 상태 공유#342

Closed
pakxe wants to merge 39 commits intodevfrom
feat/#316/share_mindmap_update
Closed

[FE] 동시 편집을 위한 마인드맵 상태 공유#342
pakxe wants to merge 39 commits intodevfrom
feat/#316/share_mindmap_update

Conversation

@pakxe
Copy link
Collaborator

@pakxe pakxe commented Feb 12, 2026

Closes #316

(작업중..)

목적

작업 내용

결과

(optional) 변경 전/후

(optional) 사용방법

(optional) 리뷰해줬으면 좋겠는 부분

(optional) 기타

pakxe added 22 commits February 11, 2026 18:37
@pakxe pakxe linked an issue Feb 12, 2026 that may be closed by this pull request
2 tasks
@vercel
Copy link

vercel bot commented Feb 12, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
web-team2-episode Ready Ready Preview, Comment Feb 12, 2026 8:57am

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @pakxe, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

이 Pull Request는 마인드맵 애플리케이션에 실시간 동시 편집 기능을 도입하기 위한 초기 단계의 구현을 포함합니다. Yjs 라이브러리를 활용하여 여러 사용자가 동시에 마인드맵을 수정할 수 있는 기반을 마련했으며, 오프라인 상태에서의 작업 내용도 안전하게 보존하고 동기화할 수 있도록 IndexedDB 연동을 추가했습니다. 이는 사용자 경험을 크게 향상시키고 협업 기능을 강화하는 중요한 변화입니다.

Highlights

  • Yjs 기반 동시 편집 기능 구현: 마인드맵에 실시간 동시 편집 기능을 추가하기 위해 Yjs 라이브러리를 통합했습니다. 이를 통해 여러 사용자가 동시에 마인드맵을 편집할 수 있게 됩니다.
  • 공유 마인드맵 관련 유틸리티 및 훅 추가: SharedMindmapController, SharedTreeContainer, SharedMindmapLayoutManager와 같은 핵심 유틸리티 클래스와 useSharedMindmap, useNode, useNodeResizeObserver, useOfflineMindmap 등의 React 훅을 새로 추가하여 Yjs 기반의 공유 마인드맵 상태 관리 및 UI 업데이트를 지원합니다.
  • 오프라인 지원 및 데이터 영속성: y-indexeddb를 사용하여 오프라인 상태에서도 마인드맵 작업 내용을 로컬 IndexedDB에 저장하고, 온라인 복귀 시 자동으로 동기화하는 기능을 구현했습니다.
  • 마인드맵 타입 정의 파일 리팩토링: 기존 mindmapType.ts 파일을 mindmap.ts로, mindmap_interaction_type.ts 파일을 mindmap_interaction.ts로 이름을 변경하고 관련 import 경로를 업데이트하여 명확성을 높였습니다.
  • 환경 변수 및 스크립트 추가: WebSocket 서버 URL을 위한 환경 변수(VITE_WS_BASE_URL)를 추가하고, Yjs WebSocket 서버를 실행하는 스크립트(yjs:server)를 package.json에 추가했습니다.
Changelog
  • frontend/package.json
    • Yjs, y-websocket, y-indexeddb 등 동시 편집 관련 의존성을 추가했습니다.
    • 개발 서버 실행 시 호스트를 지정하는 --host 옵션을 dev 스크립트에 추가했습니다.
    • Yjs WebSocket 서버를 실행하기 위한 yjs:server 스크립트를 추가했습니다.
  • frontend/pnpm-lock.yaml
    • 새로 추가된 Yjs 관련 패키지 및 그 의존성(isomorphic.js, lib0, y-protocols)에 대한 잠금 파일 항목을 업데이트했습니다.
  • frontend/src/constants/env.ts
    • WebSocket 서버의 기본 URL을 관리하기 위한 VITE_WS_BASE_URL 환경 변수를 추가했습니다.
  • frontend/src/features/mindmap/providers/MindmapProvider.tsx
    • NodeId 타입의 import 경로를 mindmapType에서 mindmap으로 변경했습니다.
  • frontend/src/features/mindmap/shared_mindmap/hooks/useNode.ts
    • 특정 노드의 데이터 변경을 구독하고 해당 노드 UI 컴포넌트의 리렌더링을 트리거하는 useNode 훅을 추가했습니다.
  • frontend/src/features/mindmap/shared_mindmap/hooks/useNodeResizeObserver.ts
    • 노드의 크기 변경을 감지하고 콜백을 실행하는 useNodeResizeObserver 훅을 추가했습니다.
  • frontend/src/features/mindmap/shared_mindmap/hooks/useOfflineMindmap.ts
    • Yjs 문서의 오프라인 영속성을 위해 IndexedDB를 사용하는 useOfflineMindmap 훅을 추가했습니다.
  • frontend/src/features/mindmap/shared_mindmap/hooks/useSharedMindmap.ts
    • Yjs 문서와 WebsocketProvider를 초기화하고 연결 상태를 관리하는 useSharedMindmap 훅을 추가했습니다.
  • frontend/src/features/mindmap/shared_mindmap/show_cases/ShowCase.tsx
    • 공유 마인드맵 기능을 시연하기 위한 MindmapShowcaseV3 컴포넌트를 추가했습니다.
    • 노드 추가, 삭제, 크기 조절 및 캔버스 패닝, 실행 취소/다시 실행 단축키 기능을 포함했습니다.
    • Yjs 연결 상태를 시각적으로 표시하는 UI를 추가했습니다.
  • frontend/src/features/mindmap/shared_mindmap/utils/SharedMindmapController.ts
    • 공유 마인드맵의 로직을 제어하고 Yjs 문서와 상호작용하는 SharedMindMapController 클래스를 추가했습니다.
    • 노드 추가, 삭제, 크기 업데이트, 내용 업데이트 및 레이아웃 새로고침 메서드를 포함했습니다.
  • frontend/src/features/mindmap/shared_mindmap/utils/SharedMindmapLayoutManager.ts
    • 공유 마인드맵 노드의 레이아웃을 계산하고 관리하는 SharedMindmapLayoutManager 클래스를 추가했습니다.
    • 서브트리 높이 캐싱 및 노드 파티션 기반 레이아웃 계산 로직을 구현했습니다.
  • frontend/src/features/mindmap/shared_mindmap/utils/SharedTreeContainer.ts
    • Yjs의 Y.Map을 사용하여 마인드맵 노드 데이터를 관리하는 SharedTreeContainer 클래스를 추가했습니다.
    • 노드 추가, 삭제, 이동, 업데이트 및 트랜잭션 처리를 담당합니다.
    • UndoManager를 사용하여 사용자 액션에 대한 실행 취소/다시 실행 기능을 지원합니다.
  • frontend/src/features/mindmap/types/mindmapType.ts
    • 파일 이름을 mindmap.ts로 변경했습니다.
  • frontend/src/features/mindmap/types/mindmap_interaction_type.ts
    • 파일 이름을 mindmap_interaction.ts로 변경하고 내부 import 경로를 업데이트했습니다.
  • frontend/src/features/mindmap/types/mindmap_room.ts
    • 마인드맵 방 ID를 위한 MindmapRoomId 타입을 정의하는 파일을 추가했습니다.
  • frontend/src/features/mindmap/utils/MindmapInteractionManager.ts
    • NodeId 및 InteractionMode 타입의 import 경로를 업데이트했습니다.
  • frontend/src/features/mindmap/utils/MindmapLayoutManager.ts
    • NodeElement 및 NodeId 타입의 import 경로를 업데이트했습니다.
    • 디버깅을 위한 console.log 문을 추가했습니다.
  • frontend/src/features/mindmap/utils/Renderer.ts
    • NodeElement 타입의 import 경로를 업데이트했습니다.
  • frontend/src/features/mindmap/utils/TreeContainer.ts
    • NodeData, NodeElement, NodeId, NodeType 타입의 import 경로를 업데이트했습니다.
  • frontend/src/shared/components/ui/sonner.tsx
    • 토스트 알림의 경고(warning) 아이콘에 --alert-bg 클래스를 추가했습니다.
    • 토스트 알림 스타일 변수(--normal-bg, --normal-text, --alert-bg, --alert-text)를 업데이트했습니다.
Activity
  • 이 Pull Request는 pakxe 님이 동시 편집 기능을 구현하기 위해 초기 코드를 작성하고 제출했습니다.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

이 PR은 Y.js를 사용하여 마인드맵의 동시 편집 기능을 구현하는 것을 목표로 합니다. 새로운 의존성을 추가하고, 상태 공유를 위한 여러 훅과 유틸리티 클래스(SharedMindmapController, SharedTreeContainer 등)를 도입했습니다. 전반적으로 협업 기능을 위한 좋은 기반을 마련했지만, 몇 가지 중요한 수정이 필요합니다. 특히, 원격 변경 사항에 대한 레이아웃 업데이트 로직, useLayoutEffect의 의존성 배열, 그리고 재귀적 노드 삭제 시의 트랜잭션 처리 방식에서 버그와 성능 문제가 발견되었습니다. 또한 코드 품질과 안정성을 높이기 위한 몇 가지 개선 사항을 제안합니다.

Comment on lines 30 to 32
if (!event.transaction.local) {
return;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

원격 변경 사항(!event.transaction.local)에 대해 레이아웃 계산을 건너뛰고 있습니다. 이로 인해 다른 사용자가 노드를 추가하거나 삭제했을 때, 내 화면에서는 레이아웃이 갱신되지 않아 노드가 겹치거나 잘못된 위치에 표시될 수 있습니다. 구조적인 변경(노드 추가, 삭제, 이동 등)에 대해서는 변경의 출처와 상관없이 레이아웃을 다시 계산해야 동시 편집 환경에서 일관된 UI를 보장할 수 있습니다.

Suggested change
if (!event.transaction.local) {
return;
}
// if (!event.transaction.local) {
// return;
// }

if (isChanged) {
onResize({ height: newHeight, width: newWidth });
}
}, [nodeId]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

useLayoutEffect의 의존성 배열이 [nodeId]로 되어 있습니다. nodeId는 일반적으로 변경되지 않는 값이므로, 이 effect는 컴포넌트가 마운트될 때 한 번만 실행됩니다. 노드의 콘텐츠가 변경되어 크기가 바뀌는 경우를 감지하려면 node 객체 자체를 의존성 배열에 추가해야 합니다. 또한, React Hook의 규칙에 따라 effect 내부에서 사용하는 모든 외부 스코프의 값들(node, onResize)을 의존성 배열에 포함하는 것이 좋습니다.

Suggested change
}, [nodeId]);
}, [node, onResize]);

Comment on lines +260 to +276
private _deleteTraverse({ nodeId }: { nodeId: NodeId }) {
const node = this.safeGetNode(nodeId);
if (!node) return;

let childId = node.firstChildId;

while (childId) {
const child = this.safeGetNode(childId);
if (!child) break;

const nextChildId = child.nextId;
this._deleteTraverse({ nodeId: childId });
childId = nextChildId;
}

this.deleteNode(nodeId);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

_deleteTraverse 메서드는 하위 노드를 삭제하기 위해 deleteNode를 재귀적으로 호출합니다. deleteNode는 각 호출마다 새로운 Y.js 트랜잭션을 생성하므로, 서브트리 전체를 삭제할 때 각 노드마다 별도의 트랜잭션이 발생하게 됩니다. 이는 매우 비효율적이며, 작업의 원자성을 깨뜨려 undo/redo 기능에 문제를 일으킬 수 있습니다. 전체 삭제 과정은 단일 트랜잭션 내에서 처리되어야 합니다.

_deleteTraverse 내부에서 this.deleteNode(nodeId)를 호출하는 대신, 트랜잭션을 생성하지 않는 내부 삭제 로직(예: this.yNodes.delete(nodeId))을 직접 호출하도록 수정하는 것을 권장합니다. 또한, private 헬퍼 메서드의 견고성을 높이고 인자 전달의 명확성을 위해, 인자를 객체 형태로 전달하고 내부에서 유효성 검사를 수행하도록 개선했습니다.

Suggested change
private _deleteTraverse({ nodeId }: { nodeId: NodeId }) {
const node = this.safeGetNode(nodeId);
if (!node) return;
let childId = node.firstChildId;
while (childId) {
const child = this.safeGetNode(childId);
if (!child) break;
const nextChildId = child.nextId;
this._deleteTraverse({ nodeId: childId });
childId = nextChildId;
}
this.deleteNode(nodeId);
}
private _deleteTraverse({ nodeId }: { nodeId: NodeId }) {
const node = this.safeGetNode(nodeId);
if (!node) return;
let childId = node.firstChildId;
while (childId) {
const child = this.safeGetNode(childId);
if (!child) break;
const nextChildId = child.nextId;
this._deleteTraverse({ nodeId: childId });
childId = nextChildId;
}
this.yNodes.delete(nodeId);
this.cachedNodes.delete(nodeId);
}
References
  1. Private helper methods should perform their own validation (e.g., null checks) to be robust and self-contained, even if the calling context is expected to perform the same checks.
  2. To prevent errors from incorrect argument order, especially for functions with multiple arguments of the same type (like coordinates), prefer passing arguments as a single object and using destructuring.

Comment on lines +8 to +11
export const ENV = {
API_BASE_URL: env.VITE_API_BASE_URL || `invalid`, // 에러나게 아무 뻥값 넣었습니다.
WS_BASE_URL: env.VITE_WS_BASE_URL || `invalid`,
} as const;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

환경 변수가 설정되지 않았을 때 'invalid' 문자열을 기본값으로 사용하는 것은 나중에 예측하기 어려운 런타임 오류를 발생시킬 수 있습니다. 애플리케이션 시작 시점에 필요한 환경 변수가 없는 경우, 명시적으로 오류를 발생시켜 빠르게 문제를 인지하고 수정할 수 있도록 하는 것이 좋습니다.

Suggested change
export const ENV = {
API_BASE_URL: env.VITE_API_BASE_URL || `invalid`, // 에러나게 아무 뻥값 넣었습니다.
WS_BASE_URL: env.VITE_WS_BASE_URL || `invalid`,
} as const;
function getEnv(key: keyof AppEnv): string {
const value = env[key];
if (value === undefined || value === '') {
throw new Error(`Missing environment variable: ${key}`);
}
return value;
}
export const ENV = {
API_BASE_URL: getEnv('VITE_API_BASE_URL'),
WS_BASE_URL: getEnv('VITE_WS_BASE_URL'),
} as const;

} catch (e) {
this.handleError(e);
}
}, "user-action");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

트랜잭션 origin으로 문자열 리터럴 'user-action'을 직접 사용하고 있습니다. 유지보수성을 높이고 오타를 방지하기 위해, 이미 정의된 TRANSACTION_ORIGINS.USER_ACTION 상수를 사용하는 것이 좋습니다. 이 문제는 moveTo 메서드에서도 동일하게 발견됩니다.

Suggested change
}, "user-action");
}, TRANSACTION_ORIGINS.USER_ACTION);

? parentRealX + parentNode.width + this.config.xGap
: parentRealX - childNode.width - this.config.xGap;

console.log(realX, childNode);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

디버깅 목적으로 추가된 console.log 문이 코드에 남아있습니다. 병합하기 전에 제거해야 합니다.

@pakxe pakxe self-assigned this Feb 13, 2026
@w0uldy0u
Copy link
Collaborator

@codex review

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b31191ae29

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +67 to +70
public deleteNode(nodeId: NodeId) {
this.container.getDoc().transact(() => {
this.container.delete({ nodeId });
});

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Recompute layout after deleting a node

deleteNode removes the subtree but never invalidates cached subtree heights or calls refreshLayout, and handleTransaction also skips layout recomputation for local transactions, so deletions leave surviving siblings at stale coordinates until some unrelated resize/content update happens. In practice, deleting a branch can leave large gaps or overlapping layout in the shared mindmap for all collaborators because no deletion path triggers a fresh layout pass.

Useful? React with 👍 / 👎.

@pakxe pakxe closed this Feb 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FE] 공유 마인드맵 변경 반영

2 participants