import { Node, NodeData } from "@/types/Node";
import { ConnectedLine } from "@/konva_mindmap/ConnectedLine";
import { Circle, Group, Text } from "react-konva";
import { useNodeListContext } from "@/store/NodeListProvider";
type NodeProps = {
parentNode?: Node;
node: Node;
depth: number;
text: string;
};
function NodeComponent({ parentNode, node, depth, text }: NodeProps) {
const { updateNode, selectNode, selectedNode } = useNodeListContext();
return (
<>
<Group
name="node"
id={node.id.toString()}
onDragMove={(e) => {
updateNode(node.id, {
...node,
location: {
x: e.target.x(),
y: e.target.y(),
},
});
}}
onDragEnd={(e) =>
updateNode(node.id, {
...node,
location: {
x: e.target.x(),
y: e.target.y(),
},
})
}
draggable
x={node.location.x}
y={node.location.y}
onClick={() => selectNode({ nodeId: node.id, parentNodeId: parentNode ? parentNode.id : null })}
>
<Circle
fill={selectedNode?.nodeId === node.id ? "orange" : "white"}
width={100}
height={100}
radius={70 - depth * 10}
stroke="black"
strokeWidth={3}
/>
<Text name="text" text={text} />
</Group>
{parentNode && (
<ConnectedLine
from={parentNode.location}
to={node.location}
fromRadius={70 - (depth - 1) * 10 + 10}
toRadius={70 - depth * 10 + 10}
/>
)}
</>
);
}
type DrawNodeProps = {
data: NodeData;
root: Node;
depth?: number;
parentNode?: any;
update?: (id: number, node: Node) => void;
};
export function DrawNodefromData({ data, root, depth = 0, parentNode }: DrawNodeProps) {
return (
<>
{/* from */}
<NodeComponent text={root.keyword} depth={depth} parentNode={parentNode} node={root} />
{/* to */}
{root.children?.map((childNode, index) => (
<DrawNodefromData data={data} key={index} root={data[childNode]} depth={depth + 1} parentNode={root} />
))}
</>
);
}
import { NodeData } from "@/types/Node";
import { useState, useCallback } from "react";
export default function useHistoryState<T>(initialState: T) {
const [history, setHistory] = useState([initialState]);
const [pointer, setPointer] = useState(0);
const saveHistory = useCallback(
(data: T) => {
const newHistory = [...history.slice(0, pointer + 1), data];
setPointer(newHistory.length - 1);
setHistory(newHistory);
},
[pointer, history],
);
const undo = useCallback(
(setData) => {
if (pointer <= 0) return;
setPointer((p) => p - 1);
setData(history[pointer - 1]);
},
[history, pointer],
);
const redo = useCallback(
(setData) => {
if (pointer >= history.length - 1) return;
setPointer((p) => p + 1);
setData(history[pointer + 1]);
},
[history, pointer],
);
return { saveHistory, undo, redo };
}
import { Button } from "@headlessui/react";
import { useEffect, useRef, useState } from "react";
import { Layer, Stage } from "react-konva";
import plusIcon from "@/assets/plus.png";
import minusIcon from "@/assets/minus.png";
import addElementIcon from "@/assets/addElement.png";
import deleteIcon from "@/assets/trash.png";
import { useNodeListContext } from "@/store/NodeListProvider";
import { DrawNodefromData } from "@/konva_mindmap/node";
import useWindowKeyEventListener from "@/hooks/useWindowKeyEventListener";
import { Node, NodeData } from "@/types/Node";
import { ratioSizing } from "@/konva_mindmap/events/ratioSizing";
import { addNode } from "@/konva_mindmap/events/addNode";
import Konva from "konva";
import { checkCollision } from "@/konva_mindmap/utils/collision";
export default function MindMapView() {
const { data, undoData: undo, redoData: redo, updateNode, selectedNode, overrideNodeData } = useNodeListContext();
const divRef = useRef<HTMLDivElement>(null);
const layer = useRef<Konva.Layer>();
useEffect(() => {
checkCollision(layer, updateNode);
}, [data]);
// const layer = useLayerEvent([["dragmove", () => checkCollision(layer, updateNode)]]);
const [dimensions, setDimensions] = useState({
scale: 1,
width: 500,
height: 500,
x: 0,
y: 0,
});
const keyMap = {
z: undo,
y: redo,
};
// const handleNodeDeleteRequest = () => {
// if (selectedNode) {
// setSelectedNode(null);
// const newData = deleteNode({ ...data }, selectedNode);
// updateNodeData(newData);
// }
// };
useWindowKeyEventListener("keydown", (e) => {
if (e.ctrlKey && keyMap[e.key]) {
keyMap[e.key]();
}
});
const deleteNode = (nodeData: NodeData, nodeId: number) => {
if (!nodeData[nodeId]) return;
const { children } = nodeData[nodeId];
children.forEach((childId) => {
deleteNode(nodeData, childId);
});
Object.values(nodeData).forEach((node: Node) => {
node.children = node.children.filter((childId) => childId !== nodeId);
});
delete nodeData[nodeId];
return nodeData;
};
function resizing() {
if (divRef.current) {
setDimensions((prevDimensions) => ({
...prevDimensions,
width: divRef.current.offsetWidth,
height: divRef.current.offsetHeight,
}));
}
}
useEffect(() => {
resizing();
const resizeObserver = new ResizeObserver(() => {
resizing();
});
if (divRef.current) {
resizeObserver.observe(divRef.current);
}
return () => resizeObserver.disconnect();
}, [divRef]);
return (
<div ref={divRef} className="relative h-full min-h-0 w-full min-w-0 rounded-xl bg-white">
<Stage
className="cursor-pointer"
width={dimensions.width}
height={dimensions.height}
scaleX={dimensions.scale}
scaleY={dimensions.scale}
x={dimensions.x}
y={dimensions.y}
draggable
onWheel={(e) => ratioSizing(e, dimensions, setDimensions)}
>
<Layer ref={layer}>{DrawNodefromData({ data: data, root: data[1], depth: data[1].depth })}</Layer>
</Stage>
<div className="absolute bottom-2 left-1/2 flex -translate-x-2/4 -translate-y-2/4 items-center gap-3 rounded-full border px-10 py-2 shadow-md">
<div className="flex items-center gap-3 border-r-2 px-5">
<Button className="h-5 w-5">
<img src={plusIcon} alt="ํ๋ํ๊ธฐ" />
</Button>
<span className="text-sm font-bold text-black">{Math.floor(dimensions.scale * 100)}%</span>
<Button className="h-5 w-5">
<img src={minusIcon} alt="์ถ์ํ๊ธฐ" />
</Button>
</div>
<Button className="w-8 border-r-2 pr-2">
<img src={addElementIcon} alt="์์ ์ถ๊ฐ" onClick={() => addNode(data, selectedNode, overrideNodeData)} />
</Button>
<Button className="h-5 w-5">
<img src={deleteIcon} alt="์์ ์ญ์ " />
</Button>
</div>
<Button className="absolute right-0 top-[-50px] rounded-lg bg-grayscale-600 px-4 py-2 text-sm font-bold">
์บ๋ฒ์ค ๋น์ฐ๊ธฐ
</Button>
</div>
);
}
import useHistoryState from "@/hooks/useHistoryState";
import { Node, NodeData } from "@/types/Node";
import { createContext, ReactNode, useContext, useEffect, useState } from "react";
export type NodeListContextType = {
data: NodeData | null;
selectedNode: { nodeId: number; parentNodeId: number } | null;
updateNode: (id: number, node: Node) => void;
overrideNodeData: (node: NodeData | ((newData: NodeData) => void)) => void;
saveHistory: (newState: NodeData) => void;
undoData: () => void;
redoData: () => void;
selectNode: ({ nodeId, parentNodeId }) => void;
};
const nodeData: NodeData = {
1: {
id: 1,
keyword: "์ ์ฌ๋ฉ๋ด",
depth: 1,
location: { x: 50, y: 50 },
children: [2, 3],
},
2: {
id: 2,
keyword: "์์",
depth: 2,
location: { x: 100, y: 100 },
children: [4, 5],
},
3: {
id: 3,
keyword: "ํ์",
depth: 2,
location: { x: 70, y: 70 },
children: [6, 7],
},
4: {
id: 4,
keyword: "๋ฉด",
depth: 3,
location: { x: 80, y: 80 },
children: [],
},
5: {
id: 5,
keyword: "๋ฐฅ",
depth: 3,
location: { x: 90, y: 90 },
children: [],
},
6: {
id: 6,
keyword: "๊ณ ๊ธฐ",
depth: 3,
location: { x: 30, y: 30 },
children: [],
},
7: {
id: 7,
keyword: "์ฐ๊ฐ",
depth: 3,
location: { x: 40, y: 40 },
children: [],
},
};
const NodeListContext = createContext<NodeListContextType | undefined>(undefined);
export function useNodeListContext() {
const context = useContext(NodeListContext);
if (!context) {
throw new Error("useNodeListContext must be used within a NodeListProvider");
}
return context;
}
export default function NodeListProvider({ children }: { children: ReactNode }) {
const [data, setData] = useState(nodeData);
const [selectedNode, setSelectedNode] = useState(null);
const { saveHistory, undo, redo } = useHistoryState<NodeData>(nodeData);
function updateNode(id: number, updatedNode: Node) {
setData((prevData) => ({
...prevData,
[id]: { ...prevData[id], ...updatedNode },
}));
}
function overrideNodeData(newData) {
setData(newData);
}
function undoData() {
undo(setData);
}
function redoData() {
redo(setData);
}
function selectNode({ nodeId, parentNodeId }: { nodeId: string; parentNodeId: string }) {
setSelectedNode({
nodeId,
parentNodeId,
});
}
return (
<NodeListContext.Provider
value={{ data, updateNode, overrideNodeData, undoData, redoData, saveHistory, selectNode, selectedNode }}
>
{children}
</NodeListContext.Provider>
);
}