AI 탐구노트

마인드맵 : 생각 정리 및 아이디어 도출 도구 본문

DIY 테스트

마인드맵 : 생각 정리 및 아이디어 도출 도구

42morrow 2024. 12. 10. 10:21

 

급변하는 시대를 살며 우리는 하루가 다르게 변화하는 기술과 새로운 정보의 물결 속에 있습니다. 우리의 삶에서 떼려야 뗄 수 없는 존재가 되어 버린 스마트폰, 태블릿, 컴퓨터와 같은 스크린을 하루 종일 쳐다 보면서 살아가죠. 그런데 이렇게 손쉽게 정보에 접근할 수 있는 환경이 오히려 우리의 생각을 제한하고 있진 않을까요? 끊임없이 흘러드는 콘텐츠를 수동적으로 소비하다 보면, 어느새 우리는 스스로 사고하는 시간을 잃어가고 있는 자신을 발견하곤 합니다.

 

특히 요즘 세대는 외부 정보를 흡수하기에는 뛰어난 환경을 가졌지만, 그것을 스스로 분석하고 자신의 것으로 만드는 능력은 점차 약해지고 있습니다. 정보는 단순히 쌓이는 것으로 끝나지 않으며 그것을 어떻게 정리하고 활용하느냐가 진정한 지식과 통찰로 이어집니다. 우리가 지금 놓치고 있는 중요한 과제는 바로 이런 분석의 힘을 기르는 것입니다. 이는 단순히 AI 기술을 배우거나 사용하는 데 국한되지 않고, 더 나아가 세상을 바라보는 새로운 관점을 얻는 데 필수적인 요소입니다.

 

이런 면에서 마인드맵은 생각을 정리하고 주관을 형성하는 데 아주 유용한 도구가 될 수 있습니다. 복잡하게 얽힌 생각을 시각적으로 표현하면 머릿속에서 막연하게 흩어져 있던 아이디어들이 자연스레 구조화됩니다. 무엇이 중요하고, 어떤 흐름으로 연결되는지를 파악하는데 이만큼 훌륭한 도구는 없다고 할 수 있죠. 실제로 마인드맵은 창의적인 아이디어를 발굴하거나, 전략적인 결정을 내릴 때도 자주 활용됩니다. 

 

우리는 스크린 속 정보의 홍수에서 벗어나기 위해 생각의 정리와 분석이라는 연습이 필요합니다. 급변하는 세상에서 중심을 잡는 법은 결국 자기 자신을 이해하는 데서 시작합니다. 마인드맵은 단순한 도구 그 이상으로, 복잡한 세상 속에서 길을 잃지 않게 도와주는 나침반과도 같은 역할을 할 수 있습니다. 사람에 따라 다르겠지만 말이죠. ^^;

 

이번 글에서는 간단한 마인드맵 기능을 할 수 있는 UI를 만들어 보고 그 과정을 소개하겠습니다. 


1.마인드맵(MindMap)이란?

마인드맵이란 생각과 아이디어를 시각적으로 정리하는 도구입니다. 중심 주제에서 관련된 생각을 가지처럼 뻗쳐 확장하는 방식으로 복잡한 정보를 구조적으로 정리하고 이를 통해 전체를 이해할 수 있도록 돕죠. 쭉 이어지는 읽어나기기 어려운 글 대신 그림, 색상, 연결선을 이용해 키워드들을 연결하는 시각적 표현을 통해 연령 구분없이 쉽게 사용할 수 있다는 장점이 있습니다. 

 

마인드맵은 영국의 교육학자이자 심리학자인 토니 부잔(Tony Buzan)이 체계화하고 대중화 시켰습니다. 초기에는 '인간의 사고 과정이 비선형적이며, 시각적, 연관적 방식으로 작동한다'는 심리학적 개념에서 출발했다고 합니다. 저도 어렸을 때 이 분이 썼던 마인드맵 관련 책을 읽은 적이 있습니다.

 

 

2.마인드맵을 사용하려면?

마인드맵 자체는 그냥 종이에 쓰는 것으로도 사용할 수 있습니다. 연필로 핵심 키워드를 하나 적고 거기서 파생되는 키워드들을 쭉 가지치기 하면서 작성하면 되니까요. 

 

소프트웨어로는 해외 제품은 MindMeister, XMind(소개글), iMindMap 등등이 있고, 국내 제품으로는 ThinkWise (심테크시스템), 알마인드(이스트소프트)와 같은 도구가 있습니다. ThinkWise의 경우는 젊은 시절 초창기 버전을 직접 구매해서 열심히 사용했었습니다. 마인드맵 외에도 조직도나 프로젝트 관리 등으로도 사용하기 좋았거든요. 알마인드의 경우, Lite 버전이 개인용은 무료로 공개되어 참 좋았었는데 2023년부터인가 서비스가 중단된 것으로 알고 있습니다. 하지만, 그 전에 공개되었던 버전을 현재도 인터넷 어디선가는 구할 수 있을텐데 그걸 이용하면 사용하는 것은 가능하지 않을까 싶습니다. 둘 다 아주 훌륭한 도구였습니다. 제 기억으론...

 

그림 : ThinkWise를 이용해 작성 가능한 각종 시각 자료 예시

 

 

3.마인드맵 기본 기능 구현해보기

 

3.1.요구사항 

 

ChatGPT에게는 다음 사항을 감안해서 mindmap을 생성하는 코드를 만들어 달라고 했습니다. 

  • fastapi + d3.js 마인드맵 차트 컴포넌트를 이용
  • 노드의 추가, 삭제, 변경, 펼침/접힘 기능을 컨택스트 메뉴로 구현
  • 펼침/접힘은 자식 노드 여부에 따라 화면 표시 다르게 
  • 생성한 마인드맵을 저장하는 기능 구현 (단, 포맷은 d3.js가 사용하는 구조 + 위치정보 + 자식노드 존재 여부 포함)
  • 노드 드래그로 위치 변경하는 기능 구현

3.2.프로젝트 구조

fastapi를 백엔드 서버로 하고 차트는 d3.js를 이용하도록 했습니다. 약간은 복잡한 구조를 가지고 있죠. 아마도 html+javascript 만으로도 구현이 가능할 것 같긴 한데 일단 python을 이용하도록 한 것은 향후 '글 또는 기사 요약 -> 키워드 추출 -> 마인드맵 전환' 과 같은 응용을 할 때 좋을 것 같아서 입니다. 

mindmap_app/
├── app.py              # api 호출을 통해 마인드맵의 구조, 데이터를 처리
├── static/       
│   ├── d3.v6.min.js    # mindmap chart 처리를 위한 d3 라이브러리
│   ├── script.js       # 화면 상에서의 사용자 처리 지원
│   └── styles.css
├── templates/
│   └── index.html      
└── saved_mindmap.json  # 저장된 마인드맵 파일

 

여기서는 단순히 만들어진 mindmap 구조를 시각적으로 가시화하는데 집중했습니다. app.py 에서 실제 마인드맵의 데이터와 구조 처리(노드추가, 삭제, 편집 등)을 수행하고 scripts.js에서는 화면 상에 가시화하고 사용자와의 상호작용을 처리한다고 보시면 되겠습니다. 

 

3.3.코드

하도 여러번 요구사항을 고치고 시행착오를 거치다 보니 코드가 생각보다 많이 길어졌습니다. 

 

app.py 파일

from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import HTMLResponse, JSONResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from pydantic import BaseModel
from typing import Optional
import uvicorn
import json
import os

app = FastAPI()

# 템플릿 설정
templates = Jinja2Templates(directory="templates")

# 정적 파일 설정
app.mount("/static", StaticFiles(directory="static"), name="static")

# 저장된 마인드맵 파일 경로
MINDMAP_FILE = "saved_mindmap.json"

# 초기 마인드맵 데이터
mindmap_data = {
    "name": "루트",
    "x": 100,  # 초기 x 위치
    "y": 100,  # 초기 y 위치
    "collapsed": False,  # 접힘 상태
    "children": [
        {
            "name": "자식 1",
            "x": 200,
            "y": 200,
            "collapsed": False,
            "children": [
                {"name": "손자 1", "x": 300, "y": 300, "collapsed": False},
                {"name": "손자 2", "x": 300, "y": 400, "collapsed": False}
            ]
        },
        {
            "name": "자식 2",
            "x": 200,
            "y": 300,
            "collapsed": False
        }
    ]
}



# 마인드맵 데이터를 파일에서 로드
def load_mindmap():
    global mindmap_data
    if os.path.exists(MINDMAP_FILE):
        with open(MINDMAP_FILE, 'r', encoding='utf-8') as f:
            mindmap_data = json.load(f)

# 마인드맵 데이터를 파일에 저장
def save_mindmap():
    with open(MINDMAP_FILE, 'w', encoding='utf-8') as f:
        json.dump(mindmap_data, f, ensure_ascii=False, indent=4)

# 데이터 로드
load_mindmap()

# Pydantic 모델 정의
class AddNodeRequest(BaseModel):
    parent: str
    name: str

class DeleteNodeRequest(BaseModel):
    name: str

class EditNodeRequest(BaseModel):
    name: str
    new_name: str

class SaveMindmapRequest(BaseModel):
    data: dict

# 메인 페이지 라우트
@app.get("/", response_class=HTMLResponse)
async def read_root(request: Request):
    return templates.TemplateResponse("index.html", {"request": request})

# 데이터 가져오기 라우트
@app.get("/data")
async def get_data():
    return JSONResponse(content=mindmap_data)

class AddNodeRequest(BaseModel):
    parent: str
    name: str
    x: Optional[int] = None  # x 위치 정보 (옵션)
    y: Optional[int] = None  # y 위치 정보 (옵션)

@app.post("/add_node")
async def add_node(request: AddNodeRequest):
    parent_name = request.parent
    node_name = request.name
    x = request.x
    y = request.y

    def find_node(node, name):
        if node["name"] == name:
            return node
        for child in node.get("children", []):
            result = find_node(child, name)
            if result:
                return result
        return None

    parent_node = find_node(mindmap_data, parent_name)
    if parent_node:
        if "children" not in parent_node:
            parent_node["children"] = []
        # 중복 노드 이름 방지
        existing_names = [child['name'] for child in parent_node['children']]
        if node_name in existing_names:
            raise HTTPException(status_code=400, detail="동일한 이름의 노드가 이미 존재합니다.")
        # 새 노드 추가 시 위치 정보 포함
        parent_node["children"].append({
            "name": node_name,
            "x": x if x is not None else parent_node.get("x", 0) + 50,
            "y": y if y is not None else parent_node.get("y", 0) + 50,
            "collapsed": False,
            "children": []
        })
        save_mindmap()
        return {"status": "success"}
    else:
        raise HTTPException(status_code=404, detail="부모 노드를 찾을 수 없습니다.")


@app.post("/delete_node")
async def delete_node(request: DeleteNodeRequest):
    if not request.name:
        raise HTTPException(status_code=422, detail="삭제할 노드 이름이 제공되지 않았습니다.")
    node_name = request.name

    def delete_node_rec(node, name):
        children = node.get('children', [])
        for i, child in enumerate(children):
            if child['name'] == name:
                del children[i]
                return True
            if delete_node_rec(child, name):
                return True
        return False

    if node_name == mindmap_data['name']:
        raise HTTPException(status_code=400, detail="루트 노드는 삭제할 수 없습니다.")

    deleted = delete_node_rec(mindmap_data, node_name)
    if deleted:
        save_mindmap()  # 삭제 후 데이터 저장
        return {"status": "success"}
    else:
        raise HTTPException(status_code=404, detail="노드를 찾을 수 없습니다.")

@app.post("/edit_node")
async def edit_node(request: EditNodeRequest):
    if not request.name or not request.new_name:
        raise HTTPException(status_code=422, detail="노드 이름 또는 새 이름이 제공되지 않았습니다.")
    node_name = request.name
    new_node_name = request.new_name

    def find_node(node, name):
        if node['name'] == name:
            return node
        for child in node.get('children', []):
            result = find_node(child, name)
            if result:
                return result
        return None

    node = find_node(mindmap_data, node_name)
    if node:
        node['name'] = new_node_name
        save_mindmap()  # 이름 변경 후 데이터 저장
        return {"status": "success"}
    else:
        raise HTTPException(status_code=404, detail="노드를 찾을 수 없습니다.")


def find_parent(node, child_name):
    for child in node.get('children', []):
        if child['name'] == child_name:
            return node
        result = find_parent(child, child_name)
        if result:
            return result
    return None

@app.post("/save_mindmap")
async def save_current_mindmap(request: SaveMindmapRequest):
    global mindmap_data
    mindmap_data = request.data

    # 위치 정보 검증 및 초기화
    def ensure_node_positions(node):
        if "x" not in node or "y" not in node:
            node["x"] = 0
            node["y"] = 0
        for child in node.get("children", []):
            ensure_node_positions(child)

    ensure_node_positions(mindmap_data)

    save_mindmap()  # 파일 저장
    return {"status": "success", "message": "마인드맵이 저장되었습니다."}


if __name__ == "__main__":
    uvicorn.run("app:app", host="0.0.0.0", port=8000, reload=True)

 

script.js 파일

const svg = d3.select("svg"),
    width = +svg.attr("width"),
    height = +svg.attr("height");

const g = svg.append("g").attr("transform", "translate(40,0)");

const tree = d3.tree().size([height, width - 160]);

const diagonal = d3.linkHorizontal()
    .x(d => d.y)
    .y(d => d.x);

let root;
let selectedNode = null;

function update(data) {
    root = d3.hierarchy(data, d => {
        if (d.collapsed) {
            return null; // 접힘 상태에서는 자식 노드를 숨김
        }
        return d.children || d._children;
    });

    const treeLayout = d3.tree().size([height, width - 160]);
    treeLayout(root);

    root.each(d => {
        if (d.data.x === null || d.data.y === null) {
            d.data.x = d.x; // d3.tree가 계산한 x 좌표
            d.data.y = d.y; // d3.tree가 계산한 y 좌표
        } else {
            d.x = d.data.x; // 기존 데이터의 x 좌표
            d.y = d.data.y; // 기존 데이터의 y 좌표
        }
    });

    const nodes = g.selectAll(".node")
        .data(root.descendants(), d => d.data.name);

    const nodeEnter = nodes.enter().append("g")
        .attr("class", "node")
        .attr("transform", d => `translate(${d.data.y},${d.data.x})`)
        .on("mousedown", nodeMouseDown)
        // .on("mouseup", nodeMouseUp)
        .on("contextmenu", nodeRightClick)
        .call(d3.drag()
            .on("start", dragStarted)
            .on("drag", dragged)
            .on("end", dragEnded));

    // 노드 시각적 구분
    nodeEnter.append("circle")
        .attr("r", 10)
        .style("fill", d => {
            // 자식 노드가 실제로 존재하는 경우 색상을 변경
            if ((d.data.children && d.data.children.length > 0) || 
                (d.data._children && d.data._children.length > 0)) {
                return "lightblue"; // 자식 노드가 있는 경우
            }
            return "white"; // 자식 노드가 없는 경우
        })
        .style("stroke", "steelblue");

    // 텍스트 추가
    nodeEnter.append("text")
        .attr("dy", 3)
        .attr("x", d => d.children || d._children ? -12 : 12)
        .style("text-anchor", d => d.children || d._children ? "end" : "start")
        .text(d => d.data.name);

    const nodeUpdate = nodeEnter.merge(nodes);

    nodeUpdate.transition()
        .duration(200)
        .attr("transform", d => `translate(${d.data.y},${d.data.x})`);

    nodes.exit().remove();

    const links = g.selectAll(".link")
        .data(root.links(), d => d.target.data.name);

    const linkEnter = links.enter().append("path")
        .attr("class", "link")
        .attr("d", diagonal);

    links.merge(linkEnter)
        .transition()
        .duration(200)
        .attr("d", diagonal);

    links.exit().remove();
}

// 노드 클릭 이벤트 처리
let clickStartTime;

// 선택된 노드 데이터를 정확히 설정
function nodeMouseDown(event, d) {
    clickStartTime = new Date().getTime();
    selectedNode = d.data.name; // 노드 이름 저장
}

// 컨텍스트 메뉴 이벤트
function showContextMenu(x, y, d) {
    if (!d || !d.data) {
        console.error("노드 데이터가 없습니다.");
        return;
    }

    const contextMenu = d3.select("#context-menu");

    // 자식 노드 유무 확인
    const hasChildren = (d.children && d.children.length > 0) || (d._children && d._children.length > 0);

    // "펼침/접힘" 항목 상태 결정
    const toggleMenuItem = hasChildren
        ? `<li onclick="toggleNodeState()">펼침/접힘</li>` // 활성화 상태
        : `<li style="color: #ccc; pointer-events: none;">펼침/접힘</li>`; // 비활성화 상태

    contextMenu.html(`
        <ul>
            <li onclick="showAddNodeModal()">노드 추가</li>
            <li onclick="deleteSelectedNode()">노드 삭제</li>
            <li onclick="showEditNodeModal()">노드 편집</li>
            ${toggleMenuItem}
        </ul>
    `);

    contextMenu.style("display", "block")
        .style("left", `${x}px`)
        .style("top", `${y}px`);
}



// 컨텍스트 메뉴 숨기기
function hideContextMenu() {
    d3.select("#context-menu").style("display", "none");
}

// 배경 클릭 시 컨텍스트 메뉴 숨기기
svg.on("click", hideContextMenu);


// 노드 추가
function showAddNodeModal() {
    const nodeName = prompt("추가할 노드 이름:");
    if (nodeName) {
        addNode(selectedNode, nodeName);
    }
}


function deleteSelectedNode() {
    if (!selectedNode) {
        alert("삭제할 노드를 선택하세요.");
        return;
    }

    fetch('/delete_node', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ name: selectedNode }) // 정확한 노드 이름 전달
    })
    .then(response => {
        if (!response.ok) {
            return response.json().then(err => {
                console.error("노드 삭제 오류:", err);
                alert("노드 삭제 실패: " + err.detail);
            });
        }
        return fetchData();
    })
    .catch(err => console.error("네트워크 오류:", err));
}

// 노드 편집
function showEditNodeModal() {
    const newNodeName = prompt("새로운 노드 이름:");
    if (newNodeName) {
        editNode(selectedNode, newNodeName);
    }
}

function editNode(oldName, newName) {
    if (!oldName || !newName) {
        alert("편집할 노드와 새로운 이름을 입력하세요.");
        return;
    }

    fetch('/edit_node', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ name: oldName, new_name: newName }) // 이름 정보 전달
    })
    .then(response => {
        if (!response.ok) {
            return response.json().then(err => {
                console.error("노드 이름 변경 오류:", err);
                alert("노드 이름 변경 실패: " + err.detail);
            });
        }
        return fetchData();
    })
    .catch(err => console.error("네트워크 오류:", err));
}

// 드래그 이벤트 처리
function dragStarted(event, d) {
    d3.select(this).raise().classed("active", true);
}

function dragged(event, d) {
    d.x += event.dy;
    d.y += event.dx;
    d3.select(this).attr("transform", `translate(${d.y},${d.x})`);
    g.selectAll(".link").attr("d", diagonal); // 링크 실시간 업데이트
}

// 드래그 종료 시 위치 정보 서버에 저장
function dragEnded(event, d) {
    d3.select(this).classed("active", false);
    d.data.x = d.x;
    d.data.y = d.y;

    // 서버에 위치 정보 업데이트
    saveMindmap();
}

function toggleNodeState() {
    if (!selectedNodeData || !selectedNodeData.data) {
        console.error("선택된 노드 데이터가 없습니다.");
        return;
    }

    selectedNodeData.data.collapsed = !selectedNodeData.data.collapsed;

    if (selectedNodeData.data.collapsed) {
        selectedNodeData._children = selectedNodeData.children;
        selectedNodeData.children = null;
    } else {
        selectedNodeData.children = selectedNodeData._children;
        selectedNodeData._children = null;
    }

    // 트리 업데이트
    update(root.data);

    // 컨텍스트 메뉴 숨기기
    hideContextMenu();

    // 상태 변경 후 저장
    saveMindmap();
}

// 서버에 마인드맵 저장
function saveMindmap() {
    const mindmap = root.data;

    // 위치 정보가 없는 노드에 기본값 설정
    function ensureNodePositions(node) {
        if (node.x === undefined || node.y === undefined) {
            node.x = 0; // 기본값
            node.y = 0; // 기본값
        }
        if (node.children) {
            node.children.forEach(ensureNodePositions);
        }
    }
    ensureNodePositions(mindmap);

    fetch('/save_mindmap', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ data: mindmap })
    })
    .then(response => {
        if (!response.ok) {
            console.error("저장 오류:", response.statusText);
            alert("위치 저장 실패");
        }
    })
    .catch(err => console.error("네트워크 오류:", err));
}


// 데이터 초기화 및 첫 렌더링
function fetchData() {
    fetch('/data')
        .then(response => response.json())
        .then(data => {
            hideContextMenu(); // 초기화 시 컨텍스트 메뉴 숨김
            update(data);
        });
}

// 배경 클릭 시 컨텍스트 메뉴 숨기기
svg.on("click", hideContextMenu);

function addNode(parentName, nodeName) {
    if (!parentName || !nodeName) {
        console.error("부모 이름 또는 새 노드 이름이 유효하지 않습니다.");
        alert("부모 노드와 추가할 노드 이름을 확인해주세요.");
        return;
    }

    fetch('/add_node', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ parent: parentName, name: nodeName })
    })
    .then(response => {
        if (!response.ok) {
            return response.json().then(err => {
                console.error("노드 추가 오류:", err);
                alert("노드 추가 중 오류가 발생했습니다.");
            });
        }
        return fetchData(); // 데이터 새로고침
    })
    .catch(err => console.error("네트워크 오류:", err));
}

// 트리 데이터에서 특정 노드 찾기
function findNode(tree, nodeName) {
    if (tree.name === nodeName) return tree;
    if (tree.children) {
        for (let child of tree.children) {
            const result = findNode(child, nodeName);
            if (result) return result;
        }
    }
    return null;
}

fetchData();

 

index.html 파일

<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <title>d3.js 마인드맵</title>
    <link rel="stylesheet" href="/static/styles.css">

    <!-- d3.js 라이브러리 로드 -->
    <script src="/static/d3.v6.min.js"></script>
    <!-- 사용자 스크립트 로드 -->
    <script src="/static/script.js"></script>
</head>
<body>
    <div id="controls">
        <button onclick="saveMindmap()">마인드맵 저장</button>
    </div>
    <svg width="960" height="600"></svg>

    <!-- 컨텍스트 메뉴 -->
    <div id="context-menu" class="context-menu">
        <ul>
            <li onclick="showAddNodeModal()">노드 추가</li>
            <li onclick="deleteSelectedNode()">노드 삭제</li>
            <li onclick="showEditNodeModal()">노드 편집</li>
            <li onclick="toggleNodeState()">펼침/접힘</li>
        </ul>
    </div>

    <!-- 노드 추가 모달 -->
    <div id="add-node-modal" class="modal">
        <div class="modal-content">
            <span class="close" onclick="closeAddNodeModal()">&times;</span>
            <h2>노드 추가</h2>
            <input type="hidden" id="add-parent-name">
            <label for="new-node-name">새 노드 이름:</label>
            <input type="text" id="new-node-name" placeholder="노드 이름" onkeypress="handleAddNodeKeyPress(event)">
            <button onclick="confirmAddNode()">추가</button>
        </div>
    </div>

    <!-- 노드 편집 모달 -->
    <div id="edit-node-modal" class="modal">
        <div class="modal-content">
            <span class="close" onclick="closeEditNodeModal()">&times;</span>
            <h2>노드 편집</h2>
            <input type="hidden" id="edit-node-name">
            <label for="edit-node-new-name">새 노드 이름:</label>
            <input type="text" id="edit-node-new-name" placeholder="새 노드 이름" onkeypress="handleEditNodeKeyPress(event)">
            <button onclick="confirmEditNode()">저장</button>
        </div>
    </div>
</body>
</html>

 

3.4.실행

 

$ uvicorn app:app --reloaded

 

3.5.결과 확인

 

실행결과는 다음과 같습니다. 기본 형태에서 노드추가, 편집, 드래그 등을 해서 작성한 것입니다. 사방으로 퍼지는 형태는 아니고 일종의 트리 구조만 지원합니다. 

 

(주의) 노드 추가나 삭제를 한 직후에 부모 노드의 색상이 변경되어야 하는데 리프레시를 해야 반영됩니다. 

 

그림 : 생성해 본 마인드맵 예시

 

 

저장된 마인드맵 json 파일

 

위의 마인드맵이 저장된 json 파일은 다음과 같습니다.  참고로 d3.js의 마인드맵 차트 데이터 구조는 아주 단순해서 위치 정보나 collapse 등의 항목이 없는데 아무래도 자유롭게 위치 이동이 필요하고 접고 펼치는 것도 필요할 것 같아서 해당 기능을 넣도록 변경했습니다. 

{
    "name": "루트",
    "children": [
        {
            "name": "자식1",
            "children": [
                {
                    "name": "손자1-3",
                    "x": 137.99998474121094,
                    "y": 267,
                    "collapsed": true
                },
                {
                    "name": "손자1-2",
                    "x": 68.85715048653736,
                    "y": 263,
                    "collapsed": false,
                    "children": [
                        {
                            "name": "증손자1-2-1",
                            "x": 26.857150486537364,
                            "y": 371,
                            "collapsed": false,
                            "children": []
                        },
                        {
                            "name": "증손자1-2-2",
                            "x": 96.85715048653736,
                            "y": 368,
                            "collapsed": false,
                            "children": []
                        }
                    ]
                },
                {
                    "name": "손자1-1",
                    "x": 12.14285714285711,
                    "y": 261,
                    "collapsed": false,
                    "children": []
                }
            ],
            "x": 66.14286477225164,
            "y": 132,
            "collapsed": false
        },
        {
            "name": "자식2",
            "x": 186.14284188406805,
            "y": 137,
            "collapsed": false,
            "children": [
                {
                    "name": "손자2-1",
                    "x": 202.14284188406805,
                    "y": 263,
                    "collapsed": false,
                    "children": []
                },
                {
                    "name": "손자2-2",
                    "x": 257.14284188406805,
                    "y": 267,
                    "collapsed": false,
                    "children": []
                }
            ]
        }
    ],
    "x": 124.14286477225164,
    "y": 12,
    "collapsed": false
}

 

 

4.정리

개인적인 생각으론 마인드맵은 전문도구를 사용하는 것이 훨씬 효과적입니다. 그렇지만, 만약 외부의 다른 도구나 데이터 연동 등이 필요하다면 얘기가 달라집니다. 물론 오픈소스 도구 가운데 Customizing을 지원하는 것들이 있다고는 하지만, 손쉽게 원하는 용도로 변신시키려면 많은 시간과 노력이 필요하겠죠. 그래서, 위와 같이 단순하지만 시가적인 표시 기능을 갖춘 코드가 가끔 필요할 수도 있을 거라 생각합니다. 

 

아무쪼록 마인드맵 도구를 잘 활용해서 자신만의 정보 정리 방식을 찾고 이를 실생활에 많이 응용해 보시길 바랍니다. : )

 

 


5.참고자료

 

ChatGPT는 데이터만, 마인드맵 생성은 다른 곳에서 만드는 방식 (소개글 링크)

- 데이터 생성은 ChatGPT에서, 화면 표시는 Markmap 사이트에서 수행 

- 사용 프롬프트 예시 :  "Create a mind map of [Your Topic]. List topics as central ideas, main branches, and sub-branches."