본문 바로가기
프로젝트/무붕 예약 사이트 프로젝트

[프로젝트][React] 예약 사이트 기능 구현하기 (when to meet)

by 꽁이꽁설꽁돌 2024. 9. 12.
728x90
반응형

목차

    이번 기능을 구현하면서 비대화형 이벤트에 대해서 많이 알아갔다.

    또한 비대화형 이벤트가 생각이상으로 까다로워서 당황스러웠다... 그래서 이번 기회에 한번 정리해보려고 한다.

     

    완성한 모습

    대충 주요 기능만 완성하였다...

     

    구현 시 고민했던 부분들

    1. 컴포넌트의 merge

     

    컴포넌트가 합쳐지는 것이 너무 까다로워서 display의 여부로 만들었고 첫인덱스의 배열크기를 늘렸다.

    처음에는 배열을 늘리고 줄이는 식으로 했는데 그렇게 하면 인덱스 다루는 것이 너무 까다로워

    배열의 크기는 바꾸지 않는 선으로 다시 진행했다.

     

    각 배열 아이템의 타입 정의

    export interface ReservationInterface {
      content: string;  //내용을 담을 곳
      time: number;  
      display: boolean;  //display여부
      height: number;  //merge한뒤 세로의 크기를 늘려주기 위한 타입
      Idx: number;  //각 인덱스를 저장해 놓아 배열을 다루기 쉽게 하기 위한 타입
      endIdx: number;  //끝 배열을 통해 배열 선택 취소 시 rollback 하기 위한 타입
      disabled: boolean;  //고정시 변경 불가를 위한 타입
      selected: boolean;  //선택 여부를 통해 표시하기 위한 타입
      weekend?: string;  //각 요일 저장을 위한 타입
    }

     

    예약을 위한 useReservationHook 

    모든 함수의 인자는 첫 인덱스를 기준으로 하여 최대한 로직을 통일 시켰고

    첫 인덱스의 배열을 늘리고 줄여 합치는 것을 구현했다.

    import { useState } from 'react';
    import { ReservationInterface } from '@/types/ReservationItemType';
    import { Option } from '@/types/ReservationItemType';
    interface reservationHookProps {
      data: ReservationInterface[];
      content: Option<string>;
    }
    
    //모든 함수 첫번째 인덱스를 기준
    export function useReservation({ data, content }: reservationHookProps) {
      const [items, setItems] = useState<ReservationInterface[]>(data);
    
      //예약 취소용
      function cancelReservation(firstIdx: number | null) {
        console.log('cancel!!');
    
        if (firstIdx === null) return;
    
        const prevItems = [...items];
        const newItems = prevItems.map((item, i) => {
          if (i >= firstIdx && i <= items[firstIdx].endIdx) {
            return { ...item, display: true, selected: false, content: '', endIdx: i, height: 1 };
          }
          return item;
        });
    
        setItems(newItems);
      }
    
      //예약 드래그 통합
      function mergeReservation(firstIdx: number | null, endIdx: number | null) {
        const prevItems = [...items];
    
        if (firstIdx === null || endIdx === null) return;
        const updatedItems = prevItems.map((item, i) => {
          if (i >= firstIdx && i <= endIdx) {
            return { ...item, display: false, height: 1, endIdx: i, content: '', selected: true };
          }
    
          return item;
        });
    
        const gap = endIdx - firstIdx;
        const bordergap = gap * 0.05;
        updatedItems[firstIdx] = {
          ...updatedItems[firstIdx],
          height: gap + bordergap + 1, // +1 to include both start and end
          endIdx: endIdx,
          display: true,
          selected: true,
          content: content.value,
        };
    
        setItems(updatedItems);
      }
    
      return {  fixingReservation, cancelReservation, setItems, mergeReservation, items };
    }

     

     

    2. 드래그와 클릭의 제어 및 드래그의 표시

     

    이게 제일 까다로웠다고 할 수 있다.... 클릭과 드래그의 구별을 잘해주어야 하고 드래그 표시를

    어떻게 할지에 대한 아이디어가 필요했다.

     

    1. 드래그의 표시

    마우스에 따라 드래그를 표시해줄려면 mouseEnter, mouseLeave로 하는 것보다는 state에 클릭된 상태를

    넣어 그 상태에 따라 drag여부를 지정해 주는 방식이 효율적이다.

     

    드래그 표시를 위한 useDraggingHook

    import { useState } from 'react';
    import { ReservationInterface } from '@/types/ReservationItemType';
    
    interface DraggingProps {
      items: ReservationInterface[];
      reservationWeekend: string;
    }
    
    //드래그 제어 함수
    export default function useDragging({ items, reservationWeekend }: DraggingProps) {
      const [selectedCells, setSelectedCells] = useState<string[]>([]);
    
      function getRange(start: number, end: number): string[] {
        const strIdx = Math.min(start, end);
        const endIdx = Math.max(start, end);
    
        const selected: string[] = [];
        for (let i = strIdx; i <= endIdx; i++) {
          selected.push(`${items[i].time}-${reservationWeekend}`);
        }
        return selected;
      }
      
      function draggingClear() {
        setSelectedCells([]);
      }
    
      return {selectedCells, setSelectedCells, draggingClear, getRange};
    }

     

    그 후 아래와 같이 선택된 셀로 표시를 해주기만 하면 된다.

    {items.map(
              (item, index) =>
                item.display && (
                  <StyledWeekendItem
                    $dragging={selectedCells.includes(`${item.time}-${reservationWeekend}`)}
                    >
                    {item.content}
                  </StyledWeekendItem>
                ),
            )}

     

    2. 드래그와 click이벤트의 구별제어

    일단 아래의 이벤트 작동 과정을 잘 이해해야 한다.

     

    -> 드래그와 클릭의 작동 방식

    드래그: onMouseDown -> onMouseOver -> onMouseUp

    클릭: onMouseDown -> onMouseUp 

     

    -> 유사하지만 다른 이벤트

    onMouseDown -> 클릭시 이벤트 발생

    onClick -> 클릭하고 마우스가 올라가는 순간 이벤트 발생

     

    그렇다면 드래그와 클릭을 구별하는 중요한 과정이 무엇인가? 바로 드래그 중간의 onMouseOver이다.

    그래서 나는 isDoing이라는 동작 state를 통해 내가 하고 있는 동작을 구별해주고 예외처리를 해주었다.

     

    핵심 아이디어는 아래와 같다.

     

    1. onMouseDown -> 드래그와 클릭에서 모두 작동하므로 stateless해야 한다.
    2. onMouseOver -> 여기서 드래그와 클릭의 상태가 나누어지므로 isDoing상태를 drag로 만들어 준다.
    3. onMouseUp -> 드래그 상태만 이 영역에 들어올수 있도록 isDoing !=='drag'로 분기처리 해준다.
    4. onMouseClick -> 클릭 상태만 이 영역에 들어올수 있도록 isDoing === 'drag'로 분기처리 해준다.
     //누르고 떼는 순간
      function handleClick(index: number) {
        if (items[index].disabled || isDoing === 'drag') return;
        const prevItems = [...items];
        if (!prevItems[index].selected) {
          prevItems[index] = { ...prevItems[index], selected: true, content: content.value };
          setItems(prevItems);
        }
        console.log('click---------', isDoing);
        modal.current?.showModal();
        handleDragStart(index);
        setIsDoing('click');
        CaculOptionSelect(index, items[index].endIdx);
      }
    
      //누르는 순간 -> 공통으로 발생
      function handleDragOpen(index: number) {
        if (items[index].disabled || items[index].selected) {
          setIsDoing('');
          return;
        }
        handleDragStart(index, reservationWeekend);
      }
    
      function handleDragOver(event: React.DragEvent<HTMLTableCellElement>, index: number) {
        if (
          dragStartIndex.index === null ||
          dragStartIndex.weekend !== reservationWeekend ||
          items[index].disabled ||
          items[index].selected
        ) {
          draggingClear();
          setIsDoing('');
          handleDragStart(null);
          return;
        }
        console.log('over.....', index);
        const selectedRange = getRange(dragStartIndex.index, index);
        setSelectedCells(selectedRange);
        if (isDoing !== 'drag') setIsDoing('drag');
        event.preventDefault();
      }
    
      //마우스가 올라가는 순간
      function handleDrop(index: number) {
        draggingClear();
        if (dragStartIndex.index === null || isDoing !== 'drag' || dragStartIndex.weekend != reservationWeekend) return;
    
        console.log('dragDrop-------', isDoing);
        const strIdx = Math.min(dragStartIndex.index, index);
        const endIdx = Math.max(dragStartIndex.index, index);
        handleDragStart(strIdx);
    
        const gapItems = items.slice(strIdx, endIdx + 1);
        if (gapItems.find((item) => item.selected || item.disabled)) {
          draggingClear();
          return;
        }
        modal.current?.showModal();
        CaculOptionSelect(strIdx, endIdx);
        mergeReservation(strIdx, endIdx);
      }
    
      //마우스가 영역을 벗어날때
      function handleMouseLeave() {
        draggingClear();
        setIsDoing('');
      }

     

    구현 시 깨달은 점

    관련된 상태는 묶는 것이 정신건강과 코드건강에 이롭다..

    상태관리를 위해서는 useEffect는 정말 지양하자 useEffect 쓰면 상태 추적이 정말 어려워진다.

     

    모달창의 시간관리 박스 상태 (start, end로 구분하고 그 안에서 현재 시간과 선택박스를 넣었다.)

      const [timeState, setTimeState] = useState({
        start: {
          time: { idx: 0, label: 'default', value: 0 },
          timeBox: { defaultTime: [] as Option<number>[], curTime: [] as Option<number>[] },
        },
        end: {
          time: { idx: 0, label: 'default', value: 0 },
          timeBox: { defaultTime: [] as Option<number>[], curTime: [] as Option<number>[] },
        },
      });

     

     

    또한 라이브러리는 만들기 전에 편리한게 있는지 미리 찾아보자 다 만들고 보면 허탈하다 ㅋㅋ..

    나는 선택 박스를 구현하면서 react-select를 썻는데 정말 편리하다.

      <Select<Option<string>>
              options={options}
              onChange={handleLabelSelect}
              defaultValue={options[0]}
              value={content}
              isClearable={true}
              isSearchable={true}
            />

     

    위와 같이 검색 기능, 지우기 기능등 내가 안만들고 편리하게 쓸 수 있다.

    https://react-select.com/home

     

    React-Select

    A flexible and beautiful Select Input control for ReactJS with multiselect, autocomplete and ajax support.

    jedwatson.github.io

     

     

     

    반응형