Skip to content

[FE] 마인드맵 통합 엔진 구현 및 컴포넌트 적용#386

Merged
arty0928 merged 59 commits intodevfrom
feat/#314/mindmap_engine
Feb 16, 2026
Merged

[FE] 마인드맵 통합 엔진 구현 및 컴포넌트 적용#386
arty0928 merged 59 commits intodevfrom
feat/#314/mindmap_engine

Conversation

@arty0928
Copy link
Collaborator

@arty0928 arty0928 commented Feb 15, 2026

Closes #314

목적

마인드맵의 개별 엔진을 core하는 하나의 통합 엔진으로 합치고, 기존 컴포넌트들을 적용하여 기획에 맞게 동작하도록 합니다.

작업 내용

❶ MindMapCore.ts

MindMapCore마인드맵 엔진의 중앙 오케스트레이터입니다.

  • 데이터(트리) 관리: TreeContainer
  • 좌표 배치: LayoutManager
  • 카메라(pan/zoom, viewBox): ViewportManager
  • 상호작용(드래그/드롭/삭제/패닝 상태머신): InteractionManager
  • 이벤트 라우팅: EventBroker
  • 성능 최적화: update 신호를 data/dragSession/frame 3개로 분리하여 UI 리렌더를 최소화

멤버 변수 역할

▶ 멤버 변수 목록 보기/접기
멤버 접근 타입 역할
tree public TreeContainer 전체 노드/트리 구조(부모/자식/형제 연결)
broker private EventBroker<MindMapEvents> Core 내부 Pub/Sub 이벤트 버스
layout private MindmapLayoutManager 트리 기반 좌표 배치 + 서브트리 높이 캐시
canvas private SVGSVGElement | null 실제 SVG 캔버스 ref
viewport private ViewportManager | null pan/zoom, viewBox 적용, screenToWorld
quadTree private QuadTree | null 공간 인덱스(확장 대비)
interaction private MindmapInteractionManager | null 드래그/드롭/패닝/삭제 상태 머신
_isInitialized private boolean initialize 완료 여부
  • 리렌더 최적화를 위한 외부 스토어
    | 멤버 | 타입 | 역할 |
    |---|---|---|
    | dataSnapshotCache | DataSnapshot | dataUpdate용 snapshot 캐시(동일하면 리렌더 방지) |
    | dragSessionSnapshotCache | DragSessionSnapshot | 드래그 시작/끝 snapshot 캐시 |
    | frameSnapshotCache | InteractionFrameSnapshot | 드래그 프레임 snapshot 캐시 |
    | dataStore / dragStore / frameStore | ExternalStore<T> | useSyncExternalStore 구독 대상(리스너 관리) |

메서드 역할

▶ 메서드 목록 보기/접기
메서드 역할
initialize(svg) SVG 준비 완료 후 엔진 결합(quadTree/viewport/interaction 생성) + 초기 sync
sync(affectedIds?) layout/quadTree 갱신 + 노드별 publish + dataUpdate 트리거
addNode(...) TreeContainer.attachTo 후 sync
moveNode(...) TreeContainer.moveTo 후 sync
deleteNode(nodeId) TreeContainer.delete 후 sync (실패 시 에러 publish)
updateNodeSize(nodeId,w,h) DOM 측정값 반영 후 sync
handleMouseDown(e) AddNode 클릭은 직접 addNode 호출 / 노드 클릭은 NODE_CLICK publish / 배경은 RAW_MOUSE_DOWN publish
getInteractionStatus() InteractionManager 상태 스냅샷 반환(드래그 UI용)
getCanvas/getBroker/getTree/getViewport/getLayout 접근자
getIsReady() 초기화 여부
▶ 업데이트 구독 메서드 목록 보기/접기
  • dataUpdate() : StaticLayer/EdgeLayer가 다시 그려져야 하는 시점에만
  • dragSessionUpdate() : DragGhostStyle 같은 “세션 경계” UI만
  • interactionFrameUpdate() : MovingFragment/DropIndicator 같은 “프레임” UI만
(추가) 메서드 역할
emitDataUpdate(nextSnapshot) snapshot 비교 후 변경시에만 리스너 notify
emitDragSessionUpdate(nextSnapshot) 드래그 시작/끝에서만 notify
emitInteractionFrameUpdate(nextSnapshot) mousemove 프레임 단위 notify
subscribeDataUpdate(listener) useSyncExternalStore 구독 함수
subscribeDragSession(listener) useSyncExternalStore 구독 함수
subscribeInteractionFrame(listener) useSyncExternalStore 구독 함수
getDataSnapshot() snapshot getter(캐시된 참조를 반환해야 함)
getDragSessionSnapshot() snapshot getter
getInteractionFrameSnapshot() snapshot getter

❷ InteractionManager.ts

MindmapInteractionManager입력 이벤트 기반 상태 머신입니다.

  • NODE_CLICK: 드래그 후보(potential_drag) 진입, 선택 토글
  • RAW_MOUSE_DOWN: 패닝(panning) 진입
  • RAW_MOUSE_MOVE: threshold 초과 시 dragging 전환 / dragging 프레임 업데이트 / drop target 계산
  • RAW_MOUSE_UP: drop 확정 → core.moveNode() 요청 / 상태 초기화
  • NODE_DELETE: 선택 노드 삭제 요청

멤버 변수 역할

▶ 멤버 변수 목록 보기/접기
멤버 타입 역할
mode InteractionMode idle/panning/potential_drag/dragging
startMousePos {x,y} 드래그/패닝 시작 위치
lastMousePos {x,y} 직전 프레임 위치(Delta 계산)
draggingNodeId NodeId | null 드래그 대상 헤드 노드
dragDelta {x,y} 월드 좌표 누적 이동량(translate)
mousePos {x,y} 현재 마우스 월드 좌표
dragSubtreeIds Set<NodeId> | null 드래그 덩어리(헤드+자손)
baseNode {targetId, direction} 드롭 가이드(ghost 위치의 기준)
selectedNodeId NodeId | null 키 삭제 대상 선택 노드

메서드 역할

▶ 메서드 목록 보기/접기
메서드 역할
setupEventListeners() broker 구독 등록
handleNodeClick(nodeId,e) potential_drag 진입
handleMouseDown(e) panning 진입
handleMouseMove(e) dragging 전환/dragDelta 갱신/drop target 갱신
updateDropTarget(e) ghost/drop target 탐색(밴드 하강 + y 삽입 규칙)
handleMouseUp(e) drop 확정 시 move 요청 + clear
getInteractionStatus() 렌더용 상태 스냅샷 반환
onDragSessionUpdate() dragging 진입/종료(시작/끝)
onInteractionFrameUpdate() dragging 중 mousemove

❸ ViewportManager.ts

SVG의 viewBox를 제어하는 카메라 매니저입니다.

  • panningHandler(dx,dy) : mouse drag로 카메라 이동
  • zoomHandler(delta, {clientX, clientY}) : 포인터 고정 줌
  • screenToWorld(clientX, clientY) : 입력좌표를 월드좌표로 변환 (드롭 탐색의 기준)

멤버 변수 역할

▶ 멤버 변수 목록 보기/접기
멤버 타입 역할
canvas SVGSVGElement viewBox 적용 대상
broker EventBroker<MindMapEvents> RAW_WHEEL, VIEWPORT_PAN/ZOOM 구독
panX/panY number 카메라 중심(월드)
zoom number 줌 배율
getWorldBounds () => Rect 월드 bounds 접근(확장 대비)

메서드 역할

▶ 메서드 목록 보기/접기
메서드 역할
applyViewBox() pan/zoom 기반 viewBox 계산 및 svg 반영
panningHandler(dx,dy) 픽셀 delta를 월드 delta로 변환 후 pan 업데이트
zoomHandler(delta,e) 마우스 포인터 고정 줌
screenToWorld(x,y) screen→world 변환
handleResize() 리사이즈 시 viewBox 재계산

❹ MindMapRenderer.tsx

MindMapRendererSVG 내부에 마인드맵을 실제로 그리는 “렌더링 진입점(Render Entry)” 입니다.

  • MindMapProvidercore.initialize(svg)를 끝낸 뒤(core.getIsReady() === true)에만 렌더를 시작합니다.
  • DOM 이벤트 브릿지 훅(useViewportEvents)을 한 번 연결하여,
    브라우저 입력이 MindMapCore.handleMouseDown()EventBroker로 들어가도록 만듭니다.
  • 렌더 트리 관점에서:
    • Static 그래프(노드/엣지)
    • 드래그 섀도우(원래 자리의 투명한 덩어리)
    • Interaction 오버레이(고스트/MovingFragment)
      를 레이어로 구성합니다.
  • MindMapRenderer가 “프레임마다 리렌더되는 곳”이 아니라,
  • 각 레이어가 useSyncExternalStore로 필요한 업데이트 신호만 구독

렌더 레이어 구조

  • static-layer: 드래그 대상 제외 원본 노드/엣지
  • shadow-fragment: 드래그 대상 subtree를 원래 자리에서 opacity 낮게 표현
  • interaction-layer: 드롭 가이드(ghost) + movingFragment(마우스 추적 덩어리)

메서드(컴포넌트) 역할

▶ 메서드 목록 보기/접기
함수 시그니처 역할 핵심 포인트
MindMapInnerRenderer () => JSX.Element | null core ready 이후 실제 레이어 렌더링 담당 내부에서 useViewportEvents()를 호출하여 DOM 이벤트 브릿지를 연결
MindMapRenderer () => JSX.Element | null core 준비 여부 게이트 `!mindmap

❺ TreeContainer.ts

트리 구조(부모/자식/형제)를 단일 Map에 저장하는 데이터 구조 레이어입니다.

멤버 변수 역할

▶ 멤버 변수 목록 보기/접기
멤버 타입 역할
nodes Map<NodeId, NodeElement> 전체 노드 저장소
rootNodeId NodeId 루트 노드 id
isThrowError boolean 에러 throw 여부

메서드 역할

▶ 메서드 목록 보기/접기
메서드 역할
generateNewNodeElement() 새 노드 생성(+Map 등록)
attachNext/attachPrev/appendChild() 형제/자식 연결
detach() 기존 연결에서 분리
moveTo() detach 후 prev/next/child로 이동
attachTo() 새 노드 생성 후 prev/next/child 부착
delete() 노드+자손 삭제
update() nodeId의 일부 필드 업데이트(새 객체 set)
getChildNodes/getChildIds() 자식 탐색
getAllDescendantIds() 자기 자신 포함 자손 Set
safeGetNode/getRootNode/getRootId/getParentId() 조회 유틸

❻ LayoutManager.ts

MindmapLayoutManager트리 구조 기반 노드 좌표(x,y) 계산기입니다.

  • 루트 기준 좌/우 파티션(addNodeDirection)을 유지하여 배치
  • 서브트리 높이 캐시로 O(N) 수준 배치 유지
  • size(폭/높이) 업데이트 발생 시 invalidate()로 캐시 무효화

멤버 변수 역할

▶ 멤버 변수 목록 보기/접기
멤버 타입 역할
treeContainer TreeContainer 트리 조회/업데이트
config {xGap,yGap} 노드 간격
subtreeHeightCache CacheMap<NodeId, number> 서브트리 높이 캐시

메서드 역할

▶ 메서드 목록 보기/접기
메서드 역할
getPartition(children) left/right 그룹 분리(사용자 의도 유지, 자동 밸런싱 X)
getSubTreeHeight(node) 서브트리 높이 계산(+캐시)
layoutPartition() 파티션 배치(위→아래)
layoutSubtree() 재귀 배치
invalidate(nodeId) 캐시 무효화(부모 방향으로 전파)
updateLayout({rootId}) 루트 (0,0) 고정 후 배치 시작

2️⃣ Providers

❶ MindmapContext.ts

Provider에서 내려줄 컨텍스트 정의

  • MindMapRefContext: { core, actions }
  • 3종 update store를 useSyncExternalStore로 직접 구독

제공 타입

타입 필드 역할
MindMapRefContextType core, actions core 접근 및 add/move/delete/updateNodeSize 액션 노출
MindMapStateContextType version core.sync 이후 UI 전체 리렌더 트리거

❷ MindmapProvider.tsx

MindMapProviderMindMapCore(엔진)를 React 트리에 주입하고,
엔진의 initialize 타이밍을 “SVG가 실제 크기를 가진 이후”로 지연시키는 Provider입니다.


초기화 이후 disconnext로 ResizeObserver가 종료되므로 최초 initialize 에만 사용되고, pan & zoom 은 viewportManager 이벤트로 처리합니다.

  • core는 useMemo로 1회 생성(리렌더 되어도 유지)
  • ResizeObserver로 SVG의 실 크기가 확보된 순간 core.initialize(svg) 실행
  • core의 onGlobalUpdate는 Provider의 setVersion(v=>v+1)로 연결되어 sync 때마다 UI 갱신

주요 내부 상태/변수

변수 타입 역할
core MindMapCore 엔진 인스턴스(싱글턴처럼 유지)
version number state 전역 렌더 트리거
actions {addNode, deleteNode, updateNodeSize, moveNode} 외부에 노출할 엔진 조작 API

3️⃣ Hooks

❶ useMindmapContext.ts

역할

Context 접근을 단순화하는 훅 모음입니다.

  • useMindMapActions() : Provider actions 접근
  • useMindMapCore() : Core 접근
  • useMindMapVersion() : StaticLayer(엣지/노드 리스트)가 dataUpdate 때만 리렌더
  • useMindMapDragSession() : DragGhostStyle이 drag start/end 때만 리렌더
  • useMindMapInteractionFrame() : MovingFragment/DropIndicator가 프레임마다 리렌더

❷ useMindmapNode.ts

개별 노드를 클릭하여 이벤트가 각각 발생하므로 이를 위한 노드 단위 구독 훅 입니다

  • subscribe: broker.subscribe({ key: nodeId, callback: onStoreChange })
  • snapshot: tree.safeGetNode(nodeId)
  • 결과: NodeItem은 “자기 nodeId 이벤트”가 publish될 때만 리렌더

❸ useViewportEvents.ts

브라우저 DOM 이벤트를 MindMap 내부로 전달하는 브릿지

  • wheel → RAW_WHEEL publish (preventDefault)
  • keydown(Delete/Backspace) → NODE_DELETE
  • mousedown(svg) → core.handleMouseDown() (타겟 해석이 필요해서 core로 직접 위임)
  • mousemove/mouseup(window) → RAW_MOUSE_MOVE/UP

4️⃣ Components

❶ MindMapRenderer.tsx

  • core ready 이후 SVG 렌더 트리 진입점
  • viewport events 연결(useViewportEvents)
  • StaticLayer는 dataUpdate 구독
  • DragGhostStyle은 dragSessionUpdate 구독
  • InteractionLayer는 interactionFrameUpdate 구독
    → MindMapRenderer 자체는 드래그 프레임마다 리렌더되지 않도록 “껍데기 역할”만 수행

❷ StaticLayer.tsx

  • 정적 그래프(모든 노드 + 모든 엣지) 렌더
  • dataUpdate가 발생했을 때만 재렌더되어, 엣지 경로/노드 리스트가 갱신됨

❸ InteractionLayer.tsx

  • 드래그 중에만 보이는 오버레이 레이어
  • DropIndicator(ghost) + MovingNodeFragment(이동 덩어리)

❹ MovingNodeFragment.tsx

  • 드래그 중인 subtree를 하나의 <g transform="translate(dx,dy)">로 묶어서 렌더
  • frameUpdate로 delta만 바뀌고, 노드 데이터 자체는 바뀌지 않음 → 움직임은 transform으로 처리

❺ DragGhostStyle.tsx (추가/최적화 핵심)

  • 드래그 세션 동안 “원본(static-graph)에서 드래그 대상 노드/엣지 opacity만 낮추는 CSS”를 <style>로 주입
  • dragSessionUpdate(start/end)에서만 style 텍스트가 바뀌도록 분리
    → mousemove 프레임마다 style 문자열이 다시 생성되는 비용 제거

❻ TempNode.tsx

  • ghost/new 노드를 위한 임시 UI 컴포넌트
  • DropIndicator에서 ghost 렌더링에 사용

4️⃣ Core 중심 연결 다이어그램

React App MindMap Core-2026-02-16-050545

마인드맵 초기화

image

addNode 클릭해서 노드 추가

image

delete (backspace || delete 키)

image

드래그 시 이벤트 흐름도

image
  • 리렌더 방지 흐름도
mermaid-drawing (8)

결과

2026-02-16.5.33.06.mov

리렌더링 개선 전/후

  • 드래깅마다 전체 리렌더링
2026-02-16.2.19.19.mov
  • 드래깅 시 movingFragment , ghostNode만 리렌더링
2026-02-16.5.48.38.mov

사용 방법

  1. 사용 위치에서 provider로 감싸고 ref를 전달한다
const MindmapPage = () => {
    const canvasRef = useRef<SVGSVGElement | null>(null);

    return (
        <MindMapProvider canvasRef={canvasRef}>
            <MindMapShowcase canvasRef={canvasRef} />
        </MindMapProvider>
    );
};
  1. 실제 마인드맵에서 외부 bg를 적용해야 캔버스에 땡땡이 css 가 적용된다.
export default function MindMapShowcase({ canvasRef }: MindMapShowcaseProps) {
    return (
        <div className="flex flex-col w-full h-screen bg-slate-100 overflow-hidden">
            <div className="flex-1 relative min-h-0 bg-[radial-gradient(#e5e7eb_1px,transparent_1px)] bg-size-[20px_20px]">
                <svg ref={canvasRef} className="w-full h-full block">
                    <MindMapRenderer />
                </svg>
            </div>
        </div>
    );
}

❗ 해결되지 않은 버그

드래그 앤 드롭 시 고스트 노드의 위치가 특정 상황에서 제대로 적용되지 않고 있습니다.

  • 문제가 발생하는 특정 상황들
    • 첫번째 / 마지막 위치에 고스트 노드가 보여야 할때, 잘 보이지 않거나 / 위치는 변하는 것 같은데 제대로 렌더링 되지 않습니다.
2026-02-15.11.48.11.mov

나의 설계

고스트노드를 어디에 띄워줄지 계산 로직 설명 (내 설계, 지금 의도대로 동작x)

  1. 마우스의 월드 좌표 계산 (viewport.screenToWorld)
  2. quadTree에서 마우스와 neareast 노드 찾기 ( 탐색 가속기 )

❗현재 코드에는 쿼드 트리 적용되어 있지 않습니다. 쿼드 트리를 적용하면 더 오류가 나서..

  1. movingFragment는 제외
  2. neareast 노드와 마우스의 x축 비교 → 어디에서 고스트 노드의 엣지가 시작할지
  3. AddNode 너비를 제거하고 Node.content에서 엣지가 출발하도록 node_geometry
  4. x 축으로 고스트 노드의 부모 노드를 결정.
  5. y 축으로 몇번째 자식으로 들어갈지 결정
    1. 자식이 없다면 child
    2. 자식이 있다면
      1. children에서 거리 비교하며, prev, next로 위치 계산
    3. 이때 고스트 노드의 부모가 루트라면 children을 가져올때 addNodeDirection을 고려해서 left group / right group에 있는 노드들만 가져온다
  6. 고스트 노드가 있는 상태로 mouse를 떼면 해당 고스트 노드의 부모의 삽입 위치로 추가되고 sync
  7. 고스트 노드가 없는 상태로 mouse를 떼면 다시 원본 위치 변화 x

관련 파일

파일 설명
ViewportManager.ts/screenToWorld 마우스 좌표 → world 좌표로 변환
InteractionManager.ts/updateDropTarget 드롭 위치 구하기
InteractionManager.ts/handleMouseMove 중 dragging case 프레임 월드 좌표 차이로 진행
DropIndicator.ts 고스트 노드 띄우기
EdgeLayer.ts 엣지 레이어의 시작점 (Node.Content 벽에서부터 출발하도록)
utils/node_geometry.ts Node.Content 벽에서 출발하도록 AddNode 너비 제거
image image

예상 원인

  1. 현재 3개의 복합 좌표계가 존재하기 때문에 좌표 변환 과정에서 불일치 발생.
  2. dragging 하는 과정에서 오차가 누적되는 것 같다.
  3. 현재 viewportManager에서 screen 좌표 -> 마인드맵 실제 좌표인 world 좌표로 변환 중
좌표 공간 좌표값 설명
Screen Space clientX / clientY 브라우저 픽셀 좌표 (마우스 이벤트가 주는 좌표)
SVG Viewport Pixel Space canvas.getBoundingClientRect() 기준 로컬 픽셀 좌표
World Space (Mindmap 좌표계) x / y 노드의 좌표, layout, quadTree가 사용하는 좌표
MouseEvent(clientX)
    ↓
ViewportManager.screenToWorld()
    ↓
World 좌표
    ↓
QuadTree.getPointsInRange()

렌더링할 때

Node.x / Node.y (World)
    ↓SVG viewBox가 알아서 화면에 매핑

이 과정에서 실제 마우스와 계산된 마우스 위치와의 오차로 고스트 노드가 띄워져야 하는 위치를 못 찾는 것 같습니다 (추측)

참고

마인드맵 탭에서 새로고침하면 마인드맵이 없어지는데 Renderer 파일에서 ctrl s 눌러야 합니다.

@vercel
Copy link

vercel bot commented Feb 15, 2026

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

Project Deployment Actions Updated (UTC)
web-team2-episode Error Error Feb 15, 2026 9:19pm

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @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

  • 마인드맵 통합 엔진 구현: MindMapCore를 마인드맵 엔진의 중앙 오케스트레이터로 구현하여 데이터 관리(TreeContainer), 좌표 배치(LayoutManager), 카메라 제어(ViewportManager), 상호작용(InteractionManager), 이벤트 라우팅(EventBroker) 및 성능 최적화를 통합했습니다.
  • 렌더링 레이어 구조화 및 최적화: MindMapRenderer를 통해 SVG 내부에 마인드맵을 렌더링하는 진입점을 구현하고, StaticLayer, DragGhostStyle, InteractionLayer, MovingNodeFragment 등으로 렌더링 레이어를 분리하여 필요한 업데이트 신호만 구독하도록 최적화했습니다. 특히 DragGhostStyle은 드래그 세션 시작/종료 시에만 스타일을 주입하여 불필요한 리렌더링을 방지합니다.
  • 사용자 상호작용 관리: InteractionManager를 상태 머신으로 구현하여 노드 클릭, 드래그 앤 드롭, 패닝, 노드 삭제 등 다양한 입력 이벤트를 처리하고, 드롭 타겟 탐색 로직을 개선했습니다.
  • 데이터 및 레이아웃 관리 개선: TreeContainer는 트리 구조를 효율적으로 관리하며, LayoutManager는 트리 기반 노드 좌표를 계산하고 서브트리 높이 캐시를 통해 성능을 향상시켰습니다. QuadTree는 공간 인덱스 역할을 합니다.
  • React 통합 및 Context/Hooks 제공: MindMapProvider를 통해 MindMapCore 엔진을 React 트리에 주입하고, useMindmapContext, useMindmapNode, useViewportEvents 등의 훅을 제공하여 React 컴포넌트에서 엔진 기능에 쉽게 접근하고 상태를 구독할 수 있도록 했습니다.
  • 드래그 앤 드롭 버그 및 좌표계 문제: 드래그 앤 드롭 시 고스트 노드의 위치가 특정 상황에서 제대로 적용되지 않는 버그가 존재하며, 이는 Screen Space, SVG Viewport Pixel Space, World Space 간의 복합적인 좌표 변환 과정에서 발생하는 오차로 추정됩니다.
Changelog
  • frontend/src/constants/mouse.ts
    • 마우스 버튼 상수에 휠 버튼(1)을 추가했습니다.
  • frontend/src/features/mindmap/components/DragGhostStyle.tsx
    • 드래그 세션 중 원본 노드의 투명도를 조절하는 스타일 컴포넌트를 추가했습니다.
  • frontend/src/features/mindmap/components/DropIndicator.tsx
    • 드래그 시 드롭될 위치를 시각적으로 표시하는 컴포넌트를 추가했습니다.
  • frontend/src/features/mindmap/components/EdgeLayer.tsx
    • 마인드맵 노드 간의 엣지를 렌더링하는 컴포넌트를 추가했습니다.
  • frontend/src/features/mindmap/components/InteractionLayer.tsx
    • 드래그 중인 노드와 드롭 인디케이터를 렌더링하는 컴포넌트를 추가했습니다.
  • frontend/src/features/mindmap/components/MindMapRenderer.tsx
    • 마인드맵의 모든 렌더링 레이어를 통합하고 관리하는 최상위 렌더러 컴포넌트를 추가했습니다.
  • frontend/src/features/mindmap/components/MovingNodeFragment.tsx
    • 드래그 중인 노드 서브트리를 마우스 움직임에 따라 렌더링하는 컴포넌트를 추가했습니다.
  • frontend/src/features/mindmap/components/StaticLayer.tsx
    • 정적인 마인드맵 노드와 엣지를 렌더링하는 컴포넌트를 추가했습니다.
  • frontend/src/features/mindmap/core/InteractionManager.ts
    • 사용자 상호작용(드래그, 패닝, 노드 클릭)을 관리하는 상태 머신 클래스를 추가했습니다.
  • frontend/src/features/mindmap/core/LayoutManager.ts
    • 레이아웃 계산 로직을 리팩토링하고, 서브트리 높이 캐시를 개선하며, 좌우 균형 정렬 로직을 제거했습니다.
  • frontend/src/features/mindmap/core/MindMapCore.ts
    • 마인드맵의 모든 핵심 기능을 통합하고 오케스트레이션하는 중앙 엔진 클래스를 추가했습니다.
  • frontend/src/features/mindmap/core/QuadTree.ts
    • QuadTree 구현을 리팩토링하여 삽입/삭제 로직을 개선하고, 트리를 초기화하는 clear 메서드를 추가했습니다.
  • frontend/src/features/mindmap/core/TreeContainer.ts
    • 트리 구조 관리 로직을 리팩토링하고, EventBrokerQuadTreeManager 의존성을 제거하며, 노드 연결 및 삭제 로직을 개선했습니다.
  • frontend/src/features/mindmap/core/ViewportManager.ts
    • SVG 뷰포트(카메라)의 패닝, 줌, 좌표 변환을 관리하는 클래스를 추가했습니다.
  • frontend/src/features/mindmap/hooks/useMindmapContext.ts
    • MindMapCore 인스턴스와 액션, 그리고 상호작용 프레임 및 드래그 세션 스냅샷을 구독하는 React 훅들을 추가했습니다.
  • frontend/src/features/mindmap/hooks/useMindmapNode.ts
    • 개별 노드의 변경 사항을 구독하여 리렌더링하는 React 훅을 추가했습니다.
  • frontend/src/features/mindmap/hooks/useViewportEvents.ts
    • 브라우저 DOM 이벤트를 MindMapCore로 전달하도록 로직을 변경하고, useViewportRef 훅 사용을 제거했습니다.
  • frontend/src/features/mindmap/hooks/useViewportRef.ts
    • 사용되지 않는 훅 파일을 제거했습니다.
  • frontend/src/features/mindmap/node/components/add_node/AddNode.tsx
    • direction prop의 타입을 AddNodeDirection으로 업데이트했습니다.
  • frontend/src/features/mindmap/node/components/add_node/AddNodeArrow.tsx
    • direction prop의 타입을 AddNodeDirection으로 업데이트했습니다.
  • frontend/src/features/mindmap/node/components/new_node/NewNode.tsx
    • 사용되지 않는 컴포넌트 파일을 제거했습니다.
  • frontend/src/features/mindmap/node/components/node/NodeItem.tsx
    • 개별 마인드맵 노드를 렌더링하고 크기를 측정하는 컴포넌트를 추가했습니다.
  • frontend/src/features/mindmap/node/components/node_center/NodeCenter.tsx
    • 루트 노드에 data-directiondata-action 속성을 추가하여 노드 추가 버튼의 동작을 정의했습니다.
  • frontend/src/features/mindmap/node/components/temp_node/TempNode.tsx
    • 임시 노드(새 노드, 고스트 노드)를 렌더링하는 컴포넌트를 추가했습니다.
  • frontend/src/features/mindmap/pages/MindmapPage.tsx
    • MindMapShowcase 컴포넌트를 사용하여 마인드맵을 렌더링하도록 페이지 구조를 변경했습니다.
  • frontend/src/features/mindmap/pages/MindmapShowcase.tsx
    • 마인드맵 렌더러를 표시하는 새로운 페이지 컴포넌트를 추가했습니다.
  • frontend/src/features/mindmap/providers/MindmapContext.ts
    • MindMapCore 인스턴스와 액션, 그리고 상태를 제공하는 React Context 정의를 추가했습니다.
  • frontend/src/features/mindmap/providers/MindmapProvider.tsx
    • MindMapCore를 초기화하고 React Context를 통해 제공하도록 Provider 로직을 리팩토링했습니다.
  • frontend/src/features/mindmap/providers/ViewportProvider.tsx
    • 사용되지 않는 Provider 파일을 제거했습니다.
  • frontend/src/features/mindmap/types/events.ts
    • 마인드맵 시스템 내의 다양한 이벤트를 정의하는 타입을 추가했습니다.
  • frontend/src/features/mindmap/types/interaction.ts
    • 사용자 상호작용 모드 및 스냅샷 관련 타입을 추가했습니다.
  • frontend/src/features/mindmap/types/mindmapType.ts
    • 사용되지 않는 타입 정의 파일을 제거했습니다.
  • frontend/src/features/mindmap/types/mindmap_interaction_type.ts
    • 사용되지 않는 타입 정의 파일을 제거했습니다.
  • frontend/src/features/mindmap/types/node.ts
    • 마인드맵 노드의 구조, 타입, 방향 등을 정의하는 타입을 추가했습니다.
  • frontend/src/features/mindmap/types/spatial.ts
    • 공간 좌표(Point, Rect) 및 뷰포트 변환(ViewportTransform) 관련 타입을 추가했습니다.
  • frontend/src/features/mindmap/utils/MindmapInteractionManager.ts
    • 사용되지 않는 유틸리티 파일을 제거했습니다.
  • frontend/src/features/mindmap/utils/Renderer.ts
    • 사용되지 않는 유틸리티 파일을 제거했습니다.
  • frontend/src/features/mindmap/utils/node_geometry.ts
    • 노드 콘텐츠의 경계 및 부모-자식 엣지 앵커를 계산하는 유틸리티 함수들을 추가했습니다.
  • frontend/src/features/mindmap/utils/path.ts
    • 베지어 경로 계산 및 노드 방향 결정 유틸리티 함수들을 추가했습니다.
  • frontend/src/features/quad_tree/QuadTreeShowCase.tsx
    • QuadTree 임포트 경로를 업데이트했습니다.
  • frontend/src/features/quad_tree/types/point.ts
    • 사용되지 않는 타입 정의 파일을 제거했습니다.
  • frontend/src/features/quad_tree/types/rect.ts
    • 사용되지 않는 타입 정의 파일을 제거했습니다.
  • frontend/src/global.css
    • 드래그 중인 요소와 정적 그래프에 대한 포인터 이벤트 스타일을 추가했습니다.
  • frontend/src/shared/utils/rect_helper.ts
    • PointRect 타입의 임포트 경로를 업데이트했습니다.
  • frontend/src/utils/EventBroker.ts
    • EventBroker를 제네릭 타입으로 변경하여 이벤트 데이터 전달을 지원하도록 개선했습니다.
Activity
  • arty0928님이 마인드맵의 개별 엔진을 통합하고 기존 컴포넌트에 적용하는 대규모 기능을 구현했습니다.
  • MindMapCore를 중심으로 한 새로운 아키텍처를 도입하여 마인드맵의 전반적인 동작 방식을 재정의했습니다.
  • 렌더링 성능 최적화를 위해 다양한 레이어 컴포넌트와 useSyncExternalStore를 활용한 세분화된 업데이트 전략을 적용했습니다.
  • 사용자 상호작용 및 드래그 앤 드롭 로직을 InteractionManager에서 상태 머신으로 관리하도록 구현했습니다.
  • 드래그 앤 드롭 시 고스트 노드 위치 버그에 대한 상세한 분석과 예상 원인을 제시했습니다.
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

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; }`);
Copy link
Contributor

Choose a reason for hiding this comment

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

high

EdgeLayer 컴포넌트에서 엣지 pathdata-edge-to 속성을 사용하고 있습니다. 하지만 여기서는 data-edge-child 선택자를 사용하여 스타일을 적용하고 있어, 드래그 중인 노드에 연결된 엣지가 흐려지지 않는 버그가 발생합니다. 선택자를 data-edge-to로 수정해야 합니다.

Suggested change
rules.push(`.static-graph path[data-edge-child="${safe}"] { opacity: 0.2; }`);
rules.push(`.static-graph path[data-edge-to="${safe}"] { opacity: 0.2; }`);

Comment on lines 20 to 21
const targetWidth = targetNode.width || 200;
const targetHeight = targetNode.height || 60;
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

노드의 기본 너비와 높이에 대한 fallback 값으로 20060 같은 매직 넘버가 사용되고 있습니다. 이 값들을 파일 상단이나 별도의 상수 파일에 DEFAULT_NODE_WIDTH, DEFAULT_NODE_HEIGHT와 같은 상수로 정의하면 가독성과 유지보수성을 높일 수 있습니다. 이 상수는 63번째 줄의 prevH fallback 값에도 재사용할 수 있습니다.

Suggested change
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
  1. Hardcoding values like 200 and 60 as 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;
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

document.querySelector(".mindmap-render-root")를 사용하여 DOM 요소에 접근하는 방식은 페이지에 여러 마인드맵이 있을 경우 예기치 않은 동작을 유발할 수 있어 취약합니다. MindMapInnerRenderer에서 useRef를 사용하여 g 요소에 대한 참조를 만들고, 이를 InteractionOverlay로 전달하는 것이 더 안정적인 방법입니다.

Comment on lines 38 to 39
private frameListeners = new Set<() => void>();
private sessionListeners = new Set<() => void>();
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

frameListenerssessionListeners 속성이 현재 코드에서 사용되지 않고 있습니다. 불필요한 코드를 제거하여 클래스를 더 깔끗하게 유지하는 것이 좋습니다.

References
  1. Unused properties like frameListeners and sessionListeners represent redundant information if they are not actively used or derived elsewhere. Removing them simplifies the class.

Comment on lines 454 to 460
getInteractionStatus(): InteractionSnapshot {
return this.interactionSnapshot;
}

getDragSessionStatus(): DragSessionSnapshot {
return this.dragSessionSnapshot;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

getInteractionStatusgetDragSessionStatus 메서드는 각각 getInteractionSnapshotgetDragSessionSnapshot 메서드와 기능이 중복됩니다. 현재 MindMapCore에서는 ...Snapshot 메서드를 사용하고 있으므로, 중복되는 ...Status 메서드를 제거하여 API를 단순화하는 것이 좋겠습니다.

References
  1. Having duplicate methods (getInteractionStatus vs getInteractionSnapshot) that serve the same purpose leads to redundant information and an unclear API. Prefer a single source of truth.

Comment on lines 116 to 121
getInteractionStatus() {
if (!this._isInitialized || !this.interaction) {
return;
}
return this.interaction.getInteractionStatus();
}
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

getInteractionStatus 메서드는 현재 MindMapCore 외부에서 사용되지 않으며, InteractionManager에서도 관련 중복 메서드를 제거할 것을 제안했습니다. 이 메서드를 제거하여 클래스의 API를 더 간결하게 만들 수 있습니다.

References
  1. An unused method like getInteractionStatus represents redundant information in the class API. Removing it simplifies the interface.

});

export default function TempNode({ type = "ghost", className, ...rest }: TempNodeProps) {
const isGhost = type == "ghost";
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

== 연산자 대신 === (엄격한 동등 연산자)를 사용하는 것이 타입 강제 변환으로 인한 예기치 않은 버그를 방지하는 데 도움이 됩니다.

Suggested change
const isGhost = type == "ghost";
const isGhost = type === "ghost";

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

this.appendChild({ parentNodeId: baseNode.id, childNodeId: movingNode.id });

P1 Badge Preserve root-side direction when moving as child

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".

@arty0928 arty0928 merged commit 442595b into dev Feb 16, 2026
4 checks passed
@arty0928 arty0928 deleted the feat/#314/mindmap_engine branch February 16, 2026 07:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FE] 마인드맵 엔진 통합: QuadTree 적용 및 시스템 아키텍처 구축

2 participants