react-task-app 만들기

Lucky Unlucky·2024년 6월 26일
0

modalActive boolean값 변경합니다.
modalActive이 true일때 task와 listid 받아옵니다.

//EditModal.tsx
import React, { useState } from 'react'
import { FiX } from 'react-icons/fi'
import { useTypedDispatch, useTypedSelector } from '../../hooks/redux'
import { setModalActive } from '../../store/slices/boardSlice';

const EditModal = () => {
  const dispatch = useTypedDispatch();
  const editingState = useTypedSelector(state => state.modal)
  const [ data, setData ] = useState(editingState);      

  const handleCloseButton = () =>{
    dispatch(
        setModalActive(false)
    )
  }


  return (
    <div>
        <div>
            <div>
                <div>{editingState.task.taskName}</div>
                <FiX onClick={handleCloseButton}/>
            </div>
            <div>제목</div>
            <input
                type = 'text'
                value={data.task.taskName}
            />
            <div>설명</div>
            <input 
                type = 'text'
                value={data.task.taskDescription}
            />
            <div>생성한 사람</div>
            <input 
                type = 'text'
                value={data.task.taskOwner}
            />
            <div>
                <button>
                    일 수정하기
                </button>
                <button>
                    일 삭제하기
                </button>
            </div>
        </div>
      
    </div>
  )
}

export default EditModal

input부분 타이핑 기능 추가합니다.
타이핑 할때마다 해당 task의 state값을 변경합니다.

//EditModal.tsx
import React, { ChangeEvent, useState } from 'react'
import { FiX } from 'react-icons/fi'
import { useTypedDispatch, useTypedSelector } from '../../hooks/redux'
import { deleteTask, setModalActive, updateTask } from '../../store/slices/boardSlice';
import { v4 } from 'uuid';
import { addLog } from '../../store/slices/loggerSlice';

const EditModal = () => {
  const dispatch = useTypedDispatch();
  const editingState = useTypedSelector(state => state.modal)
  const [ data, setData ] = useState(editingState);      

  const handleCloseButton = () =>{
    dispatch(
        setModalActive(false)
    )
  }

  const handleNameChange = (e: ChangeEvent<HTMLInputElement>) => {
    setData({
        ...data,
        task: {
            ...data.task,
            taskName: e.target.value
        }
    })
  }
  
  const handleDescriptionChange = (e: ChangeEvent<HTMLInputElement>) => {
    setData({
        ...data,
        task: {
            ...data.task,
            taskDescription: e.target.value
        }
    })
  }

  const handleAuthorChange = (e: ChangeEvent<HTMLInputElement>) => {
    setData({
        ...data,
        task: {
            ...data.task,
            taskOwner: e.target.value
        }
    })
  }

  const handleUpdate = () => {
    dispatch(
        updateTask({
            boardId: editingState.boardId,
            listId: editingState.listId,
            task: data.task
        })
    )

    dispatch(
        addLog({
            logId: v4(),
            logMessage: `일 수정하기: ${editingState.task.taskName}`,
            logAuthor: 'User',
            logTimestamp: String(Date.now())
        })
    )

    dispatch(setModalActive(false))
  }

  const handleDelete = () =>{
    dispatch(
        deleteTask({
            boardId: editingState.boardId,
            listId: editingState.listId,
            taskId: editingState.task.taskId
        })
    )
    dispatch(
        addLog({
            logId: v4(),
            logMessage: `일 삭제하기: ${editingState.task.taskName}`,
            logAuthor: 'User',
            logTimestamp: String(Date.now())
        })
    )
    dispatch(setModalActive(false))
  }


  return (
    <div>
        <div>
            <div>
                <div>{editingState.task.taskName}</div>
                <FiX onClick={handleCloseButton}/>
            </div>
            <div>제목</div>
            <input
                type = 'text'
                value={data.task.taskName}
                onChange={handleNameChange}
            />
            <div>설명</div>
            <input 
                type = 'text'
                value={data.task.taskDescription}
                onChange={handleDescriptionChange}
            />
            <div>생성한 사람</div>
            <input 
                type = 'text'
                value={data.task.taskOwner}
                onChange={handleAuthorChange}
            />
            <div>
                <button onClick={handleUpdate}>
                    일 수정하기
                </button>
                <button onClick={handleDelete}>
                    일 삭제하기
                </button>
            </div>
        </div>
      
    </div>
  )
}

export default EditModal

일 수정하기 버튼과, 삭제하기 버튼 기능을 위해 boardSlice에 코드를 추가합니다.

        updateTask: (state, {payload}: PayloadAction<TAddTaskAction>) => {
            state.boardArray = state.boardArray.map(board =>
                board.boardId === payload.boardId
                ? {
                    ...board,
                    lists: board.lists.map(list =>
                        list.listId === payload.listId
                        ? {
                            ...list,
                            tasks: list.tasks.map( task =>
                                task.taskId === payload.task.taskId
                                ? payload.task
                                : task
                            )
                        }
                        :list
                    )
                }
                :board
            )
        },

        deleteTask: (state, {payload}: PayloadAction<TDeleteTaskAction>) => {
            state.boardArray = state.boardArray.map(board =>
                board.boardId === payload.boardId
                ? {
                    ...board,
                    lists: board.lists.map(list => 
                        list.listId === payload.listId
                        ? {
                            ...list,
                            tasks: list.tasks.filter(
                                task => task.taskId !== payload.taskId
                            )
                        }
                        :list
                    )
                }
                : board
            )
        },

logger 만들기

import React, { FC } from 'react'
import { useTypedSelector } from '../../hooks/redux'
import { FiX } from 'react-icons/fi'
import LogItem from './LogItem/LogItem'
import { body, closeButton, header, modalWindow, title, wrapper } from './LoggerModal.css'

type TLoggerModalProps = {
setIsLoggerOpen : React.Dispatch<React.SetStateAction<boolean>>
}

const LoggerModal: FC<TLoggerModalProps> = ({
setIsLoggerOpen
}) => {

const logs = useTypedSelector(state => state.logger.logArray);

return (
  <div className={wrapper}>
    <div className={modalWindow}>
      <div className={header}>
        <div className={title}>활동 기록</div>
        <FiX className={closeButton} onClick={() => setIsLoggerOpen(false)}/>
      </div>
      <div className={body}>
        {logs.map((log) => (
          <LogItem key = {log.logId} logItem={log} />
        ))}
      </div>
    </div>
  </div>
)
}

export default LoggerModal

LogItem 컴포넌트 생성

import React, { FC } from 'react'
import { ILogItem } from '../../../types'
import { BsFillPersonFill } from 'react-icons/bs';

type TLogItemProps = {
  logItem: ILogItem;
}

const LogItem: FC<TLogItemProps> = ({
  logItem
}) => {
  const timeOffset = new Date(Date.now() - Number(logItem.logTimestamp));

  const showOffsetTime = `
    ${timeOffset.getMinutes() > 0 ? `${timeOffset.getMinutes()}m` : ''} 
    ${timeOffset.getSeconds() > 0 ? `${timeOffset.getSeconds()}s ago` : ''}
    ${timeOffset.getSeconds() === 0 ? `now` : ''} 
  `
  return (
    <div>
      <div>
        <BsFillPersonFill />
        {logItem.logAuthor}
      </div>
      <div>{logItem.logMessage}</div>
      <div>{showOffsetTime}</div>
    </div>
  )
}

export default LogItem

게시판 삭제
board의 배열이 1보다 클때만 삭제를 하며, 최소 1개를 남기게 하도록 조건을 걸어줍니다.
앞 게시판이 삭제가 되면 뒤에 게시판이 자동으로 activeBoardId로 되게 함수를 추가해 줍니다.

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 } from './store/slices/boardSlice';
import { addLog } from './store/slices/loggerSlice';
import { v4 } from 'uuid';

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개입니다.')
    }
  }

  return (
    <div className={appContainer}>

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

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

export default App

게시판 삭제를 적용하기 위하여 BoardSlice에 코드를 추가 입력해줍니다.

        deleteBoard: (state, {payload}: PayloadAction<TDeleteBoardAction>) => {
            state.boardArray = state.boardArray.filter(
                board => board.boardId !== payload.boardId
            )
        },

css.ts파일들을 이용해 꾸며줍니다.

//EditModal.css.ts
import { style } from "@vanilla-extract/css";
import { vars } from "../../App.css";

export const wrapper = style({
    width: '100vw',
    height: '100vh',
    display: 'flex',
    justifyContent: 'center',
    alignItems: 'center',
    position: 'absolute',
    zIndex: 10000
})

export const modalWindow = style({
    display: 'flex',
    flexDirection: 'column',
    alignItems: 'center',
    width: '800px',
    height: 'max-content',
    maxHeight: '500px',
    overflowY: 'auto',
    backgroundColor: vars.color.mainDarker,
    opacity: 0.95,
    borderRadius: 14,
    padding: 20,
    boxShadow: vars.shadow.basic,
    color: vars.color.brightText
})

export const header = style({
    width: '100%',
    display: 'flex',
    alignItems: 'center',
    justifyContent: 'center',
    marginBottom: '40px'
})

export const closeButton = style({
    fontSize: vars.fontSizing.T2,
    cursor: 'pointer',
    marginTop: '-20px',
    ":hover": {
        opacity: 0.8
    }
})

export const title = style({
    fontSize: vars.fontSizing.T2,
    color: vars.color.brightText,
    marginRight: 'auto',
    marginBottom: vars.spacing.medium    
})

export const buttons = style({
    display: 'flex',
    justifyContent: 'space-around',
    marginBottom: 50
})

export const updateButton = style({
    border: 'none',
    borderRadius: 5,
    fontSize: vars.fontSizing.T4,
    padding: vars.spacing.big2,
    marginRight: vars.spacing.big1,
    backgroundColor: vars.color.updateButton,
    cursor: 'pointer',
    ":hover": {
        opacity: 0.8
    }
})

export const deleteButton = style({
    border: 'none',
    borderRadius: 5,
    fontSize: vars.fontSizing.T4,
    padding: vars.spacing.big2,
    marginRight: vars.spacing.big1,
    backgroundColor: vars.color.deleteButton,
    cursor: 'pointer',
    ":hover": {
        opacity: 0.8
    }
})

export const input = style({
    width: "100%",
    minHeight: '30px',
    border: 'none',
    borderRadius: 5,
    marginBottom: vars.spacing.big2,
    padding: vars.spacing.medium,
    fontSize: vars.fontSizing.T4,
    boxShadow: vars.shadow.basic
})
//LoggerModal.css.ts
import { style } from "@vanilla-extract/css";
import { vars } from "../../App.css";

export const wrapper = style({
    width: '100vw',
    height: '100vh',
    display: 'flex',
    justifyContent: 'center',
    alignItems: 'center',
    position: 'absolute',
    zIndex: 10000
})

export const modalWindow = style({
    display: 'flex',
    flexDirection: 'column',
    alignItems: 'center',
    width: '800px',
    height: 'max-content',
    maxHeight: '500px',
    overflowY: 'auto',
    borderRadius: '14px',
    padding: 20,
    boxShadow: vars.shadow.basic,
    backgroundColor: vars.color.mainDarker,
    opacity: 0.95,
    color: vars.color.brightText
})

export const header = style({
    width: '100%',
    display: 'flex',
    alignItems: 'center',
    justifyContent: 'center',
    marginBottom: '40px'
})

export const title = style({
    fontSize: vars.fontSizing.T2,
    color: vars.color.brightText,
    marginRight: 'auto',
    marginBottom: vars.spacing.medium
})

export const closeButton = style({
    fontSize: vars.fontSizing.T2,
    cursor: 'pointer',
    marginTop: '-20px',
    ":hover": {
        opacity: 0.8
    }
})

export const body = style({
  maxHeight: '400px',
  overflowY: 'auto',
  width: '100%'  
})
//LogItem.css.ts
import { style } from "@vanilla-extract/css";
import { vars } from "../../../App.css";

export const logItemWrap = style ({
    display: 'flex',
    flexDirection: 'column',
    alignSelf: 'flex-start',
    padding: vars.spacing.medium,
    marginBottom: vars.spacing.big2,
    width: '100%',
    borderBottom: 'solid 1px rbg(191, 197, 217, 0.3)',
    ":hover": {
        backgroundColor: vars.color.mainFadedBright,
        borderRadius: 10
    }
})

export const message = style ({
    display: 'flex',
    flexDirection: 'row',
    alignItems: 'center',
    color: vars.color.brightText,
    fontWeight: 'bold',
    fontSize: vars.fontSizing.T4,
    marginBottom: vars.spacing.small
})

export const author = style ({
    display: 'flex',
    alignItems: 'center',
    columnGap: 10,
    color: vars.color.brightText,
    fontSize: vars.fontSizing.T3,
    fontWeight: 'bold',
    marginBottom: vars.spacing.medium
})

export const date = style ({
    fontSize: vars.fontSizing.T4,
    fontWeight: 'bold',
    marginBottom: vars.spacing.medium
})
profile
늒네입니다.

0개의 댓글