마인드맵(1)

Franklee·2023년 5월 1일
0

Project

목록 보기
1/2
post-thumbnail
  • React
  • Redux Toolkit
  • Typescript
  • Lodash

1. 노드의 자유로운 추가 및 삭제
2. 노드 설정
3. 노드와 노드를 이어주는 선 존재

1. 노드의 자유로운 추가 및 삭제

먼저 마인드맵을 구성하는 노드(엘리먼트)를 제작한다.

노드 기본 정보

매우 간단하고 간편하게 시작할 수 있는 마인드맵의 제작이 목표이기에 노드가 보여주는 정보는 간단 텍스트, 내포하고 있는 데이터는 위치 및 id 그리고 부모 노드의 id 이다. 여러개의 노드를 편리하게 편집하고 사용하기위해 redux-toolkit을 사용해 노드에 대한 정보를 배열 형태로 저장 및 수정 할 수 있도록 한다.

interface MindNode {
  id:string //id
  location:{ //위치 좌표
    x:number;
    y:number;
  }
  from:string //부모노드의 id
}

여기서 id의 생성은 처음에는 해당 엘리먼트를 map()을 이용해 해당 메소드 내부에서 주어지는 index에 문자열을 추가해 사용하려고 했으나 노드의 생성 순서가 규칙적이지 않고 from에 부모노드의 id가 사전에 필요하기 때문에 redux에 추가할때 생성하는 것으로 한다.

이때, 랜덤적으로 겹치지 않을(가능성이 낮은) 메소드를 사용하여 id를 생성하였다.

export const randomID = () => {
  var fourChars = function () {
    return (((1 + Math.random()) * 0x10000) | 0)
      .toString(16)
      .substring(1)
      .toUpperCase();
  };
  return (
    fourChars() +
    fourChars() +
    '-' +
    fourChars() +
    '-' +
    fourChars() +
    '-' +
    fourChars() +
    '-' +
    fourChars() +
    fourChars() +
    fourChars()
  );
};
// 출처 http://stackoverflow.com/questions/226689/unique-element-id-even-if-element-doesnt-have-one
// '00000000-0000-0000-0000-000000000000' 형태의 id생성

노드 생성 및 삭제

노드 생성 및 삭제에 대해서는 초기에는 drag&drop을 사용해 추가는 일반적인 버튼을 이용해 추가를 하고, 삭제는 특정영역에 drop을 하게되면 해당 노드를 삭제하는 방식을 이용하려고 했었으나, 매우 간단하게 이용할 수 있도록 만들자는 취지를 생각해 각 노드의 상단마다 생성, 삭제 버튼을 추가하는 방식으로 설정하였다.

삭제 및 생성 모두 노드 내부의 redux 상태관리 함수를 통해 실행시킬 수 있으며, 노드의 id를 매개변수로 받아 새로 생성되는 노드의 from에 저장되도록 한다.

    //생성
    addEle: (state, action: PayloadAction<string>) => {
          let temp = state.element;
          const ran = {
            id: randomID(),// 랜덤id 생성 메소드
            location: { x: 0, y: 0 },
            from: action.payload //생성을 실행시킨 노드가 마인드맵에서 부모노드가 되기에 from에 id를 저장
          };
          temp.push(ran);
          state.element = temp;
        },

	//삭제
      delEle: (state, action: PayloadAction<string>) => {
      let temp = state.element;
      _.remove(temp, (data) => data.id === action.payload);//해당 노드를 temp에서 삭제
      state.element = temp;
    },

2. 노드 설정

Ondrag

drag 관련해서는 dragStart, drag, dragEnd, dragOver를 사용했으며, 실질적으로 Ondrag를 중점적으로 사용하게 된다.

//pathType

  const [location, setLocation] = useState<pathType>({
    id: data.id,
    x: data.location.x,
    y: data.location.y
    //data는 map()사용시 노드 매개변수로 들어오는 redux 상태할당 데이터. 초기 location은 x,y = (0,0);
  });

  const dragEndHandler = (e: React.DragEvent<HTMLDivElement>) => {
        const pos = { ...location };
    pos['x'] = e.clientX - 45;
    pos['y'] = e.clientY - 20;//현재 마우스 커서 위치 e.clientX(Y) -45,-20은 노드 넓이(높이)/2 값을 빼줌으로서 노드 중심 이동
    setLocation(pos);
    dispatch(editLocation(location)); //현재 위치를 id와 같이 redux-location 저장
  };

Ondrag에서 redux 상태를 변경시킴으로

 editLocation: (state, action: PayloadAction<pathType>) => {
      const temp = state.element;
      const index = _.findIndex(temp, (data) => {
        return data.id === action.payload.id;
      });//해당 id가진 index 추출
      temp[index].location = { x: action.payload.x, y: action.payload.y };//해당 index의 location변경

      state.element = temp;
    }

3. 노드와 노드를 이어주는 선

line 기본설정

선을 만들기 위해 div태그를 넓이를 0px, 높이는 노드간의 간격으로 사용하고, border-left를 이용해 한쪽에만 테두리를 넣음으로서 한개의 선과 같은 효과를 내도록한다.

  position: absolute;
  width: 0px;
  border-left: 1px dashed blue;

그리고 top, left, height, transform(rotate)에 대한 값을 설정해야 하는데 이는 부모노드와 이동하고자 하는 노드의 위치값을 통해 계산하여 값을 할당 하게 된다.

  • top, left는 기준이 되는 노드의 위치값이 되며,
 const fromObj: xy = {
        x: location.x + 48.5 + 'px',
        y: location.y + 20 + 'px'
      }; // 연결선 초기 시작점(하위) element로 부터 시작한다 + 48.5(널이 / 2)와 20(높이 / 2)은 element의 크기에서 정 중앙에서 시작하기 위함

      setFrom(fromObj); //초기위치 설정
  • height는 두 노드 사이의 길이를 삼각형의 빗변 구하는 방식을 통해 계산한다.
const height = Math.sqrt(state.x * state.x + state.y * state.y);
  • 그리고 노드간의 각도를 계산하기 위해 Math.atan(역탄젠트)를 이용하여 각도를 계산하도록 한다.
const state = {
        x: location.x - otherLocation.x,//기준노드와 대상노드 거리 x값
        y: whichIsBigger(otherLocation.y, location.y) //기준노드와 대상노드 거리 y값
      };

      const tempTan = ((Math.atan(state.x / state.y) * 180) / Math.PI).toFixed(
        1
      );

다만, 각도가 -90~90(deg)까지 밖에 계산이 안되기 때문에 기준y값 > 대상y값 이게되면,
선의 위치가 기준--대상이 아닌 --기준 대상 처럼 선이 대상을 바라보는것이 아닌 바깥을 향하게 된다.

여기서 기준대상을 맞바꾸게 된다면 ( A(기준)-B(대상) => B(기준)-A(대상) ) 이전 기준y값 > 대상y값에서 발생했던 문제가 해결될수 있다. 그러므로 기준y값 > 대상y값이 되는 기점에 두 노드를 맞바꿔 준다면 정상적인 해결이 가능하다.

const cal = (myElement: ElementObj) => {
    if (myElement.from === null) {
      return; //fromID가 null일시 return => 오직 HEAD에 적용(예정)
    }

    const at = _.findIndex(ele, (data) => {
      return data.id === myElement.from;
    }); //도착 element 위치 계산위한 ele의 인덱스 찾기 - 찾는곳은 redux의 location에서 직접 검색

    /*
    아래의 조건문에 따라서 선의 시작지점이 바뀐다. => 

    하위element가 시작element일시 시작점 yS가 끝점 yE관계에서 

    yS >= yE 일때 정상작동을 하게되지만 
    yS < yE 일때, 비정상적인 작동을 하게된다. 
    
    이러한 이유는 transform의 rotate의 각도가 -90~90을 넘지않기에 발생되는 현상이다.

    이를 해결하기위해 기존 시작과 대상 element을 서로 바꿔주면서 rotate의 각도가 각 시작지점에 따라 정상작동하도록 설계하였다. 
    */

    const location = myElement.location;
    const otherLocation = ele[at].location;

    if (location.y >= otherLocation.y) {
      const fromObj: xy = {
        x: otherLocation.x + 48.5 + 'px',
        y: otherLocation.y + 20 + 'px'
      }; // 연결선 초기 시작점(상위) element로 부터 시작한다 + 48.5(널이 / 2)와 20(높이 / 2)은 element의 크기에서 정 중앙에서 시작하기 위함

      setFrom(fromObj); //초기위치 설정

      const state = {
        x: otherLocation.x - location.x,
        y: whichIsBigger(location.y, otherLocation.y)
      };

      const tempTan = ((Math.atan(state.x / state.y) * 180) / Math.PI).toFixed(
        1
      ); //역탄젠트를 이용한 각도계산
      setTan(parseInt(tempTan));

      const real = Math.sqrt(state.x * state.x + state.y * state.y); //피타고라스를 이용한 대각선 계산
      setTo(real);
    } else {
      const fromObj: xy = {
        x: location.x + 48.5 + 'px',
        y: location.y + 20 + 'px'
      }; // 연결선 초기 시작점(하위) element로 부터 시작한다 + 48.5(널이 / 2)와 20(높이 / 2)은 element의 크기에서 정 중앙에서 시작하기 위함

      setFrom(fromObj); //초기위치 설정

      const state = {
        x: location.x - otherLocation.x,
        y: whichIsBigger(otherLocation.y, location.y)
      };

      const tempTan = ((Math.atan(state.x / state.y) * 180) / Math.PI).toFixed(
        1
      ); //역탄젠트를 이용한 각도계산
      setTan(parseInt(tempTan));

      const real = Math.sqrt(state.x * state.x + state.y * state.y); //피타고라스를 이용한 대각선 계산
      setTo(real);
    }
  };
profile
복잡한 문제를 쉬운 코드로 해결해 나가는 개발자

0개의 댓글