Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Summary of ChangesHello @arty0928, 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! 이 PR은 마인드맵 기능을 위한 핵심 엔진을 통합하고, React 컴포넌트와의 효율적인 연동을 위한 아키텍처를 구축합니다. 기존의 개별적인 엔진 요소들을 MindMapCore 아래로 통합하여 중앙 집중식으로 관리하며, 렌더링 성능 최적화를 위해 레이어별 업데이트 메커니즘을 도입했습니다. 또한, 사용자 상호작용을 정교하게 제어하는 상태 머신을 구현하고, 데이터 구조 및 레이아웃 계산 로직을 개선하여 마인드맵의 전반적인 기능성과 반응성을 향상시키는 데 중점을 두었습니다. Highlights
Changelog
Activity
Using Gemini Code AssistThe 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
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 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
|
There was a problem hiding this comment.
Code Review
This pull request introduces a comprehensive refactor of the mind map's rendering and interaction logic, centralizing state management and event handling. Key changes include the creation of a MindMapCore class to orchestrate TreeContainer, MindmapLayoutManager, QuadTree, ViewportManager, and MindmapInteractionManager instances. The rendering pipeline is now split into StaticLayer, MovingNodeFragment, DropIndicator, and DragGhostStyle components, with InteractionOverlay managing dynamic elements and MindMapRenderer as the main entry point. Interaction events (mouse, keyboard, wheel) are now handled by an EventBroker and processed by MindmapInteractionManager, which tracks various interaction modes and calculates drop targets. The MindmapLayoutManager was updated to use addNodeDirection for partitioning children and refined node positioning calculations. The QuadTree implementation was also refactored for better point insertion and boundary handling. Additionally, new React hooks (useMindMapInteractionFrame, useMindMapDragSession, useMindMapNode) were introduced to subscribe to specific interaction states and node data, replacing older context-based approaches. Review comments highlighted the need to correct a CSS selector in DragGhostStyle for ghosted edges, replace magic numbers with constants in DropIndicator, use a useRef for the root SVG element instead of document.querySelector in MindMapRenderer, remove unused frameListeners and sessionListeners from InteractionManager, and eliminate redundant getInteractionStatus/getDragSessionStatus methods across the codebase, along with a suggestion to use strict equality (===) in TempNode.
| dragSubtreeIds.forEach((id) => { | ||
| const safe = escapeAttr(String(id)); | ||
| rules.push(`.static-graph [data-node-id="${safe}"] { opacity: 0.2; }`); | ||
| rules.push(`.static-graph path[data-edge-child="${safe}"] { opacity: 0.2; }`); |
There was a problem hiding this comment.
EdgeLayer 컴포넌트에서 엣지 path에 data-edge-to 속성을 사용하고 있습니다. 하지만 여기서는 data-edge-child 선택자를 사용하여 스타일을 적용하고 있어, 드래그 중인 노드에 연결된 엣지가 흐려지지 않는 버그가 발생합니다. 선택자를 data-edge-to로 수정해야 합니다.
| rules.push(`.static-graph path[data-edge-child="${safe}"] { opacity: 0.2; }`); | |
| rules.push(`.static-graph path[data-edge-to="${safe}"] { opacity: 0.2; }`); |
| const targetWidth = targetNode.width || 200; | ||
| const targetHeight = targetNode.height || 60; |
There was a problem hiding this comment.
노드의 기본 너비와 높이에 대한 fallback 값으로 200과 60 같은 매직 넘버가 사용되고 있습니다. 이 값들을 파일 상단이나 별도의 상수 파일에 DEFAULT_NODE_WIDTH, DEFAULT_NODE_HEIGHT와 같은 상수로 정의하면 가독성과 유지보수성을 높일 수 있습니다. 이 상수는 63번째 줄의 prevH fallback 값에도 재사용할 수 있습니다.
| const targetWidth = targetNode.width || 200; | |
| const targetHeight = targetNode.height || 60; | |
| const targetWidth = targetNode.width || DEFAULT_NODE_WIDTH; | |
| const targetHeight = targetNode.height || DEFAULT_NODE_HEIGHT; |
References
- Hardcoding values like
200and60as fallback dimensions in a component reduces flexibility and maintainability. These should be defined as constants or passed as props.
|
|
||
| // mode가 바뀔 때만 DOM의 속성을 변경 (리렌더링 유발 X) | ||
| useEffect(() => { | ||
| const root = document.querySelector(".mindmap-render-root") as HTMLElement; |
| private frameListeners = new Set<() => void>(); | ||
| private sessionListeners = new Set<() => void>(); |
There was a problem hiding this comment.
| getInteractionStatus(): InteractionSnapshot { | ||
| return this.interactionSnapshot; | ||
| } | ||
|
|
||
| getDragSessionStatus(): DragSessionSnapshot { | ||
| return this.dragSessionSnapshot; | ||
| } |
There was a problem hiding this comment.
getInteractionStatus와 getDragSessionStatus 메서드는 각각 getInteractionSnapshot과 getDragSessionSnapshot 메서드와 기능이 중복됩니다. 현재 MindMapCore에서는 ...Snapshot 메서드를 사용하고 있으므로, 중복되는 ...Status 메서드를 제거하여 API를 단순화하는 것이 좋겠습니다.
References
- Having duplicate methods (
getInteractionStatusvsgetInteractionSnapshot) that serve the same purpose leads to redundant information and an unclear API. Prefer a single source of truth.
| getInteractionStatus() { | ||
| if (!this._isInitialized || !this.interaction) { | ||
| return; | ||
| } | ||
| return this.interaction.getInteractionStatus(); | ||
| } |
There was a problem hiding this comment.
| }); | ||
|
|
||
| export default function TempNode({ type = "ghost", className, ...rest }: TempNodeProps) { | ||
| const isGhost = type == "ghost"; |
There was a problem hiding this comment.
💡 Codex Review
In moveTo, the child path reattaches via appendChild without passing an addNodeDirection. appendChild defaults missing direction to "right" for root parents, so dropping a node onto the root's left side gets persisted on the right branch instead. This causes drag-and-drop to produce the opposite tree structure whenever the root is the drop parent (for example, when inserting into an empty left branch).
ℹ️ About Codex in GitHub
Codex has been enabled to automatically 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 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
…bootcamp-7th/Team2-Episode into feat/#314/mindmap_engine
Closes #314
목적
마인드맵의 개별 엔진을 core하는 하나의 통합 엔진으로 합치고, 기존 컴포넌트들을 적용하여 기획에 맞게 동작하도록 합니다.
작업 내용
❶ MindMapCore.ts
MindMapCore는 마인드맵 엔진의 중앙 오케스트레이터입니다.TreeContainerLayoutManagerViewportManagerInteractionManagerEventBrokerdata/dragSession/frame3개로 분리하여 UI 리렌더를 최소화멤버 변수 역할
▶ 멤버 변수 목록 보기/접기
treeTreeContainerbrokerEventBroker<MindMapEvents>layoutMindmapLayoutManagercanvasSVGSVGElement | nullviewportViewportManager | nullquadTreeQuadTree | nullinteractionMindmapInteractionManager | null_isInitializedboolean| 멤버 | 타입 | 역할 |
|---|---|---|
|
dataSnapshotCache|DataSnapshot| dataUpdate용 snapshot 캐시(동일하면 리렌더 방지) ||
dragSessionSnapshotCache|DragSessionSnapshot| 드래그 시작/끝 snapshot 캐시 ||
frameSnapshotCache|InteractionFrameSnapshot| 드래그 프레임 snapshot 캐시 ||
dataStore / dragStore / frameStore|ExternalStore<T>|useSyncExternalStore구독 대상(리스너 관리) |메서드 역할
▶ 메서드 목록 보기/접기
initialize(svg)sync(affectedIds?)addNode(...)moveNode(...)deleteNode(nodeId)updateNodeSize(nodeId,w,h)handleMouseDown(e)getInteractionStatus()getCanvas/getBroker/getTree/getViewport/getLayoutgetIsReady()▶ 업데이트 구독 메서드 목록 보기/접기
emitDataUpdate(nextSnapshot)emitDragSessionUpdate(nextSnapshot)emitInteractionFrameUpdate(nextSnapshot)subscribeDataUpdate(listener)useSyncExternalStore구독 함수subscribeDragSession(listener)useSyncExternalStore구독 함수subscribeInteractionFrame(listener)useSyncExternalStore구독 함수getDataSnapshot()getDragSessionSnapshot()getInteractionFrameSnapshot()❷ InteractionManager.ts
MindmapInteractionManager는 입력 이벤트 기반 상태 머신입니다.core.moveNode()요청 / 상태 초기화멤버 변수 역할
▶ 멤버 변수 목록 보기/접기
modeInteractionModestartMousePos{x,y}lastMousePos{x,y}draggingNodeIdNodeId | nulldragDelta{x,y}mousePos{x,y}dragSubtreeIdsSet<NodeId> | nullbaseNode{targetId, direction}selectedNodeIdNodeId | null메서드 역할
▶ 메서드 목록 보기/접기
setupEventListeners()handleNodeClick(nodeId,e)handleMouseDown(e)handleMouseMove(e)updateDropTarget(e)handleMouseUp(e)getInteractionStatus()onDragSessionUpdate()onInteractionFrameUpdate()❸ ViewportManager.ts
SVG의 viewBox를 제어하는 카메라 매니저입니다.
panningHandler(dx,dy): mouse drag로 카메라 이동zoomHandler(delta, {clientX, clientY}): 포인터 고정 줌screenToWorld(clientX, clientY): 입력좌표를 월드좌표로 변환 (드롭 탐색의 기준)멤버 변수 역할
▶ 멤버 변수 목록 보기/접기
canvasSVGSVGElementbrokerEventBroker<MindMapEvents>panX/panYnumberzoomnumbergetWorldBounds() => Rect메서드 역할
▶ 메서드 목록 보기/접기
applyViewBox()panningHandler(dx,dy)zoomHandler(delta,e)screenToWorld(x,y)handleResize()❹ MindMapRenderer.tsx
MindMapRenderer는 SVG 내부에 마인드맵을 실제로 그리는 “렌더링 진입점(Render Entry)” 입니다.MindMapProvider가core.initialize(svg)를 끝낸 뒤(core.getIsReady() === true)에만 렌더를 시작합니다.useViewportEvents)을 한 번 연결하여,브라우저 입력이
MindMapCore.handleMouseDown()및EventBroker로 들어가도록 만듭니다.를 레이어로 구성합니다.
MindMapRenderer가 “프레임마다 리렌더되는 곳”이 아니라,useSyncExternalStore로 필요한 업데이트 신호만 구독렌더 레이어 구조
메서드(컴포넌트) 역할
▶ 메서드 목록 보기/접기
MindMapInnerRenderer() => JSX.Element | nulluseViewportEvents()를 호출하여 DOM 이벤트 브릿지를 연결MindMapRenderer() => JSX.Element | null❺ TreeContainer.ts
트리 구조(부모/자식/형제)를 단일 Map에 저장하는 데이터 구조 레이어입니다.
멤버 변수 역할
▶ 멤버 변수 목록 보기/접기
nodesMap<NodeId, NodeElement>rootNodeIdNodeIdisThrowErrorboolean메서드 역할
▶ 메서드 목록 보기/접기
generateNewNodeElement()attachNext/attachPrev/appendChild()detach()moveTo()attachTo()delete()update()getChildNodes/getChildIds()getAllDescendantIds()safeGetNode/getRootNode/getRootId/getParentId()❻ LayoutManager.ts
MindmapLayoutManager는 트리 구조 기반 노드 좌표(x,y) 계산기입니다.addNodeDirection)을 유지하여 배치invalidate()로 캐시 무효화멤버 변수 역할
▶ 멤버 변수 목록 보기/접기
treeContainerTreeContainerconfig{xGap,yGap}subtreeHeightCacheCacheMap<NodeId, number>메서드 역할
▶ 메서드 목록 보기/접기
getPartition(children)getSubTreeHeight(node)layoutPartition()layoutSubtree()invalidate(nodeId)updateLayout({rootId})2️⃣ Providers
❶ MindmapContext.ts
Provider에서 내려줄 컨텍스트 정의
MindMapRefContext:{ core, actions }useSyncExternalStore로 직접 구독제공 타입
MindMapRefContextTypecore,actionsMindMapStateContextTypeversion❷ MindmapProvider.tsx
MindMapProvider는 MindMapCore(엔진)를 React 트리에 주입하고,엔진의 initialize 타이밍을 “SVG가 실제 크기를 가진 이후”로 지연시키는 Provider입니다.
초기화 이후 disconnext로 ResizeObserver가 종료되므로 최초 initialize 에만 사용되고, pan & zoom 은 viewportManager 이벤트로 처리합니다.
useMemo로 1회 생성(리렌더 되어도 유지)ResizeObserver로 SVG의 실 크기가 확보된 순간core.initialize(svg)실행onGlobalUpdate는 Provider의setVersion(v=>v+1)로 연결되어 sync 때마다 UI 갱신주요 내부 상태/변수
coreMindMapCoreversionnumber stateactions{addNode, deleteNode, updateNodeSize, moveNode}3️⃣ Hooks
❶ useMindmapContext.ts
역할
Context 접근을 단순화하는 훅 모음입니다.
useMindMapActions(): Provider actions 접근useMindMapCore(): Core 접근useMindMapVersion(): StaticLayer(엣지/노드 리스트)가 dataUpdate 때만 리렌더useMindMapDragSession(): DragGhostStyle이 drag start/end 때만 리렌더useMindMapInteractionFrame(): MovingFragment/DropIndicator가 프레임마다 리렌더❷ useMindmapNode.ts
개별 노드를 클릭하여 이벤트가 각각 발생하므로 이를 위한 노드 단위 구독 훅 입니다
broker.subscribe({ key: nodeId, callback: onStoreChange })tree.safeGetNode(nodeId)❸ useViewportEvents.ts
브라우저 DOM 이벤트를 MindMap 내부로 전달하는 브릿지
RAW_WHEELpublish (preventDefault)NODE_DELETEcore.handleMouseDown()(타겟 해석이 필요해서 core로 직접 위임)RAW_MOUSE_MOVE/UP4️⃣ Components
❶ MindMapRenderer.tsx
dataUpdate구독dragSessionUpdate구독interactionFrameUpdate구독→ MindMapRenderer 자체는 드래그 프레임마다 리렌더되지 않도록 “껍데기 역할”만 수행
❷ StaticLayer.tsx
❸ InteractionLayer.tsx
❹ MovingNodeFragment.tsx
<g transform="translate(dx,dy)">로 묶어서 렌더❺ DragGhostStyle.tsx (추가/최적화 핵심)
<style>로 주입→ mousemove 프레임마다 style 문자열이 다시 생성되는 비용 제거
❻ TempNode.tsx
4️⃣ Core 중심 연결 다이어그램
마인드맵 초기화
addNode 클릭해서 노드 추가
delete (backspace || delete 키)
드래그 시 이벤트 흐름도
결과
2026-02-16.5.33.06.mov
리렌더링 개선 전/후
2026-02-16.2.19.19.mov
2026-02-16.5.48.38.mov
사용 방법
❗ 해결되지 않은 버그
2026-02-15.11.48.11.mov
나의 설계
고스트노드를 어디에 띄워줄지 계산 로직 설명 (내 설계, 지금 의도대로 동작x)
node_geometryViewportManager.ts/screenToWorldInteractionManager.ts/updateDropTargetInteractionManager.ts/handleMouseMove 중 dragging caseDropIndicator.tsEdgeLayer.tsutils/node_geometry.ts예상 원인
clientX / clientYcanvas.getBoundingClientRect()x / y렌더링할 때
이 과정에서 실제 마우스와 계산된 마우스 위치와의 오차로 고스트 노드가 띄워져야 하는 위치를 못 찾는 것 같습니다 (추측)
참고
마인드맵 탭에서 새로고침하면 마인드맵이 없어지는데 Renderer 파일에서 ctrl s 눌러야 합니다.