react-task-app 만들기

Lucky Unlucky·2024년 6월 27일
0

React Beautiful Dnd
터미널에 npm i react-beautiful-dnd 입력하여 모듈 설치

import logo from './logo.svg';
import { useState } from 'react'
import { DragDropContext } from 'react-beautiful-dnd'
import './App.css';

const finalSpaceCharacters = [
  {
    id: 'gray',
    name: 'Gary Goodspeed'
  },
  {
    id: 'cato',
    name: 'Little Cato'
  },
  {
    id: 'abc',
    name: 'ABC'
  }
]


function App() {

  const [characters, setCharacters] = useState(finalSpaceCharacters)

  const handleEnd = (result) =>{
    //result 매개변수에는 source 항목 및 대상 위치와 같은 Drag 이벤트에 대항 정보가 포함
    console.log(result)

    if(!result.destination) return;
    
    //리액트 불변성을 지켜주기 위해 새로운 todoDate생성
    const items = Array.from(characters);

    const [reorderedItem] = items.splice(result.source.index, 1)
    //변경시키려는 item을 배열에서 지우고 return값으로 지워진 item을 가져온다.

    //원하는 자리에 reorderedItem을 insert해준다.
    items.splice(result.destination.index, 0, reorderedItem);
    setCharacters(items)
  }

  return (
    <div className="App">
      <header className="App-header">
        <h1>Final Space Characters</h1>
        <DragDropContext onDragEnd={handleEnd}>
            <Droppable droppableId='characters'>
              {(provided) => (
              <ul className='characters' {...provided.droppableProps}
                ref={provided.innerRef}>
                {
                  characters.map(({id, name}, index) => {
                    return (
                      <Draggable key={id} draggable={id} index={index}>
                        {(provided) => (
                        <li ref={provided.innerRef}
                          {...provided.draggableProps}
                          {...provided.dragHandleProps}
                        >
                          <p>
                            {name}
                          </p>
                        </li>
                        )}
                      </Draggable>
                    )
                  })
                }
                {provided.placeholder}
              </ul>
              )}
          </Droppable>
        </DragDropContext>
      </header>
    </div>
  );
}

export default App;

Drag & Drop

Drag와 Drop을 구현하기 위해 최상위범위인 App.tsx에 DragDropContext를 선언해줍니다.
그 다음 List.tsx에 Droppable를 선언해줍니다.
마지막 범위이자 객체인 task에 Draggable을 선언해줍니다.

board에서 변화가 생기는 것이기에 boardSlice에 sort를 추가해줍니다.
같은리스트에서 옮길경우와 다른리스트로 옮길 경우를 모두 고려해야하기에 분기처리로 작성을 해줍니다.

//App.tsx
import { useState } from 'react'
import { appContainer, board, buttons, deleteBoardButton, loggerButton } from './App.css'
import BoardList from './components/BoardList/BoardList'
import ListsContainer from './components/ListsContainer/ListsContainer';
import { useTypedDispatch, useTypedSelector } from './hooks/redux';
import EditModal from './components/EditModal/EditModal';
import LoggerModal from './components/LoggerModal/LoggerModal';
import { deleteBoard, sort } from './store/slices/boardSlice';
import { addLog } from './store/slices/loggerSlice';
import { v4 } from 'uuid';
import { DragDropContext } from 'react-beautiful-dnd';

function App() {

  const dispatch = useTypedDispatch();
  const [isLoggerOpen, setIsLoggerOpen] = useState(false);
  const [activeBoardId, setActiveBoardId] = useState('board-0');
  const modalActive = useTypedSelector(state => state.boards.modalActive);

  const boards = useTypedSelector(state => state.boards.boardArray)
  //특정 객체만 가져온다.
  const getActiveBoard = boards.filter(board => board.boardId === activeBoardId)[0]; //클릭할때 마다 바뀌는값

  const lists = getActiveBoard.lists;

  const handleDeleteBoard = () => {
    if(boards.length > 1) {
      dispatch(
        deleteBoard({boardId: getActiveBoard.boardId})
      )
      dispatch(
        addLog({
          logId: v4(),
          logMessage: `게시판 지우기: ${getActiveBoard.boardName}`,
          logAuthor: 'User',
          logTimestamp: String(Date.now())
        })
      )

      //activeBoardId를 넘겨준다
      const newIndexToSet = () => {
        const indexToBeDeleted = boards.findIndex(
          board => board.boardId === activeBoardId
        )
        return indexToBeDeleted === 0
        ? indexToBeDeleted + 1
        : indexToBeDeleted - 1;
      }
      //newIndexToSet의 boardId를 ActiveBoardId에 셋팅한다.
      setActiveBoardId(boards[newIndexToSet()].boardId)
    } else {
      alert('최소 게시판의 개수는 1개입니다.')
    }
  }

  const handleDragEnd = (result: any) => {
    console.log(result);
    const {destination, source, draggableId} = result;
    
    console.log('lists', lists)
    const sourceList = lists.filter(
      list => list.listId === source.droppableId
    )[0];

    console.log('source list', sourceList);
      dispatch(
        sort({
          boardIndex: boards.findIndex(board => board.boardId == activeBoardId),
          droppableIdStart: source.droppableId,
          droppableIdEnd: destination.droppableId,
          droppableIndexStart: source.index,
          droppableIndexEnd: destination.index,
          draggableId
        })
      )

      dispatch(
        addLog({
          logId: v4(),
          logMessage: `
          리스트 "${sourceList.listName}" 에서
          리스트 "${lists.filter(list => list.listId === destination.droppableId)[0].listName}으로
          ${sourceList.tasks.filter(task => task.taskId === draggableId)[0].taskName}을 옮겼습니다.
          `,
          logAuthor: 'User',
          logTimestamp: String(Date.now())
        })
      )
  }

  return (
    <div className={appContainer}>

      {isLoggerOpen ? <LoggerModal setIsLoggerOpen={setIsLoggerOpen} /> : null}
      {modalActive ? <EditModal /> : null}

      <BoardList activeBoardId={activeBoardId} setActiveBoardId={setActiveBoardId}/>
      <div className={board}>
      <DragDropContext onDragEnd={handleDragEnd}>
        <ListsContainer lists = {lists} boardId = {getActiveBoard.boardId}/>
      </DragDropContext>
      </div>
      <div className={buttons}>
        <button className={deleteBoardButton} onClick={handleDeleteBoard}>
          이 게시판 삭제하기
        </button>
        <button className={loggerButton} onClick={() => setIsLoggerOpen(!isLoggerOpen)}>
          {isLoggerOpen ? "활동 목록 숨기기" : "활동 목록 보이기"}
        </button>
      </div>
    </div>
  )
}

export default App
//list.tsx
import React, { FC } from 'react'
import { GrSubtract } from 'react-icons/gr'
import Task from '../Task/Task'
import ActionButton from '../ActionButton/ActionButton'
import { IList, ITask } from '../../types'
import { useTypedDispatch } from '../../hooks/redux'
import { deleteList, setModalActive } from '../../store/slices/boardSlice'
import { addLog } from '../../store/slices/loggerSlice'
import { v4 } from 'uuid'
import { setModalData } from '../../store/slices/modalSlice'
import { deleteButton, header, listWrapper, name } from './List.css'
import { Droppable } from 'react-beautiful-dnd'

type TListProps = {
  boardId: string;
  list: IList;
}



const List: FC<TListProps> = ({
  list,
  boardId
}) => {

  const dispatch = useTypedDispatch();

  const handleListDelete = (listId: string) => { 
    dispatch(deleteList({boardId, listId}));
    dispatch(
      addLog({
        logId: v4(),
        logMessage: `리스트 삭제하기 : ${list.listName}`,
        logAuthor: 'User',
        logTimestamp: String(Date.now())
      })
    )
  }

  const handleTaskChange = (
    boardId: string,
    listId: string,
    taskId: string,
    task: ITask
  ) => {

    dispatch(setModalData({boardId, listId, task}))
    dispatch(setModalActive(true));
  }

  return (
    <Droppable droppableId={list.listId}>
      {provided => (
    <div
      {...provided.droppableProps}
      ref={provided.innerRef}
      className={listWrapper}
    >
      <div className={header}>
        <div className={name}>{list.listName}</div>
          <GrSubtract 
            className={deleteButton}
            onClick={() => handleListDelete(list.listId)}
          />
      </div>
        {list.tasks.map((task, index) => (
          <div
          onClick={() => handleTaskChange(boardId, list.listId, task.taskId, task)}
            key = {task.taskId}
          >
              <Task
                taskName = {task.taskName}
                taskDescription = {task.taskDescription}
                boardId = {boardId}
                id = {task.taskId}
                index = {index}
              />
            </div>
        ))}
        {provided.placeholder}
         <ActionButton 
          boardId={boardId}
          listId={list.listId} 
         /> {/*Pops로 분기처리*/}
    </div> 
    )}
    </Droppable>
  )
}

export default List;
//Task.tsx
import React, { FC } from 'react'
import { container, description, title } from './Task.css';
import { Draggable } from 'react-beautiful-dnd';

type TTaskProps = {
  index: number;
  id: string;
  boardId: string;
  taskName: string;
  taskDescription: string;
}

const Task: FC<TTaskProps> = ({
  index,
  id,
  boardId,
  taskName,
  taskDescription
}) => {
  return (
    <Draggable draggableId={id} index={index}>
    {provided => (
    <div className={container}
      ref={provided.innerRef}
      {...provided.draggableProps}
      {...provided.dragHandleProps}   
    >
      <div className={title}>{taskName}</div>
      <div className={description}>{taskDescription}</div>
    </div>
    )}
    </Draggable>
  )
}

export default Task
//boardSlice.ts
~~~~~
  sort: (state, {payload}: PayloadAction<TSortAction>) => {
            if(payload.droppableIdStart === payload.droppableIdEnd) {
                const list = state.boardArray[payload.boardIndex].lists.find(
                    list => list.listId === payload.droppableIdStart
                )
                
                const card = list?.tasks.splice(payload.droppableIndexStart, 1);
                    list?.tasks.splice(payload.droppableIndexEnd, 0, ...card!)
            }

            if(payload.droppableIdStart !== payload.droppableIdEnd) {
                const listStart = state.boardArray[payload.boardIndex].lists.find(
                    list => list.listId === payload.droppableIdStart
                )

                const card = listStart!.tasks.splice(payload.droppableIndexStart, 1);
                const listEnd = state.boardArray[payload.boardIndex].lists.find(
                    list => list.listId === payload.droppableIdEnd
                )
                listEnd?.tasks.splice(payload.droppableIndexEnd, 0, ...card);
            }
        }
    }
})

export const { sort, addBoard, deleteList, setModalActive, addList, addTask, updateTask, deleteTask, deleteBoard } = boardsSlice.actions;
export const boardsReducer = boardsSlice.reducer;

FireBase연결
FireBase 홈페이지에 가서 프로젝트 생성 후
웹으로 추가를 해줍니다.

VScode로 가서 firebase 패키지를 설치해줍니다.

firebase.ts를 생성 후
위에 화면에 나왔던 내용을 복사 후 붙여넣고 외부로 내보낼 수 있도록
마지막 줄을 export const app = initializeApp(firebaseConfig);로 바꿔 준 후 저장합니다.

login 만들기

//BoardList.tsx
const auth = getAuth(app);
  const provider = new GoogleAuthProvider();

  const handleLogin = () => {
    signInWithPopup(auth, provider)
    .then(userCrendential => {
      console.log(userCrendential);
        dispatch(
              setUser({
                email: userCrendential.user.email,
                id: userCrendential.user.uid
              })
            )
        })
        .catch(error => {
          console.error(error);
      })
  }
      
    })
  }
 		 <GoSignOut className={addButton} />
         <FiLogIn className={addButton} onClick={handleLogin}/>

google로그인을 하기 위해 BoardList.tsx에 위의 코드를 추가합니다
아래 2줄은 로그인과 로그아웃 버튼입니다.

프로젝트 생성한 firebase에서 Authentication에 들어간 후 로그인 제공업체를 선택 후 이메일 or 계정을 입력 하고 나서 저장합니다.

유저 데이터를 전역으로 사용하기 위해 userSlice를 만들어줍니다.

//userSlice.ts
import { createSlice } from "@reduxjs/toolkit";

const initialState = {
    email: '',
    id: ''
}

const userSlice = createSlice({
    name: 'user',
    initialState,
    reducers: {
        setUser: (state, action) => {
            state.email = action.payload.email,
            state.id = action.payload.id
        }

    }
})

export const {setUser} = userSlice.actions;
export const userReducer = userSlice.reducer;

Slice로 받은 데이터를 redux에 넣어줍니다.

로그아웃을 위해 Hook에 useAuth.ts를 만듭니다.
useAuth는 로그인 유무를 확인하는 역할을 합니다. redux에 있는 유저 데이터를 가져옵니다.

import { useTypedSelector } from "./redux"

export function useAuth() {
    
    const { id, email } = useTypedSelector((state) => state.user);
    
    //로그인이 되어있는지 안되어있는지, 되어있으면 email과 id출력
    return {
        isAuth: !!email,
        email,
        id
    }
}
import { createSlice } from "@reduxjs/toolkit";

const initialState = {
    email: '',
    id: ''
}

const userSlice = createSlice({
    name: 'user',
    initialState,
    reducers: {
        setUser: (state, action) => {
            state.email = action.payload.email,
            state.id = action.payload.id
        },

        removeUser: (state) => {
            state.email = '';
            state.id = '';
        }

    }
})


export const {setUser, removeUser} = userSlice.actions;
export const userReducer = userSlice.reducer;

로그아웃을 하기위해 removeUser를 선언하고 이메일과 id값을 공백으로 state시키도록합니다.

import React, { FC, useRef, useState } from 'react'
import { useTypedDispatch, useTypedSelector } from '../../hooks/redux';
import { FiLogIn, FiPlusCircle } from 'react-icons/fi';
import SideForm from './SideForm/SideForm';
import { addSection, container, title, addButton, boardItemActive, boardItem } from './BoardList.css';
import clsx from 'clsx';
import { GoSignOut } from 'react-icons/go';
import { GoogleAuthProvider, getAuth, signInWithPopup, signOut } from 'firebase/auth';
import { app } from '../../firebase';
import { removeUser, setUser } from '../../store/slices/userSlice';
import { useAuth } from '../../hooks/useAuth';

type TBoardListProps = {
  activeBoardId: string;
  setActiveBoardId:
  React.Dispatch<React.SetStateAction<string>>
}


const BoardList: FC<TBoardListProps> = ({
  activeBoardId,
  setActiveBoardId
}) => {

  const dispatch = useTypedDispatch();
  const { boardArray } = useTypedSelector(state => state.boards);
  const [isFormOpen, setIsFormOpen] = useState(false);
  const inputRef = useRef<HTMLInputElement>(null);

  const auth = getAuth(app);
  const provider = new GoogleAuthProvider();

  const { isAuth } = useAuth();

  const handleLogin = () => {
    signInWithPopup(auth, provider)
    .then(userCrendential => {
      console.log(userCrendential);
        dispatch(
          setUser({
            email: userCrendential.user.email,
            id: userCrendential.user.uid
          })
        )
    })
    .catch(error => {
      console.error(error);
    })
  }

  const handleClick = () => {
    setIsFormOpen(!isFormOpen)
    inputRef.current?.focus();
  }

  const handleSignOut = () => {
    signOut(auth)
    .then(() => {
      dispatch(
        removeUser()
      )
    })
    .catch((error) => {
      console.error(error);
    })
  }

  return (
    <div className={container}>
      <div className={title}>
        게시판 : 
      </div>
      {boardArray.map((board, index) => (
        <div key={board.boardId}
        onClick={() => setActiveBoardId(boardArray[index].boardId)}
          className= {
            clsx(
              {
                [boardItemActive]:
                boardArray.findIndex(board => board.boardId === activeBoardId) === index 
              },
              {
                [boardItem]:
                boardArray.findIndex(board => board.boardId === activeBoardId) !== index
              }
            )
          }
         >
          <div>
            {board.boardName}
          </div>
        </div>
      ))}
      <div className={addSection}>
        {
          isFormOpen ?
          <SideForm inputRef={inputRef} setIsFormOpen = {setIsFormOpen} />
          :
          <FiPlusCircle className={addButton}onClick={handleClick}/>
        }

          { isAuth
          ?
          <GoSignOut className={addButton} onClick={handleSignOut}/>
          :
          <FiLogIn className={addButton} onClick={handleLogin}/>
          }
      </div>
    </div>
  )
}

export default BoardList

firebase 서비스를 이용하기 위해 firebase-tools를 전역으로 설치해줍니다.
npm install -g firebase-tools

profile
늒네입니다.

0개의 댓글