React-task-app 만들기

Lucky Unlucky·2024년 6월 25일
0

Board List 생성

//BoardList.tsx
import React, { FC, useState } from 'react'
import { useTypedSelector } from '../../hooks/redux';
import { FiPlusCircle } from 'react-icons/fi';
import SideForm from './SideForm/SideForm';

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


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

  const { boardArray } = useTypedSelector(state => state.boards);
  const [isFormOpen, setIsFormOpen] = useState(false);

  return (
    <div>
      <div>
        게시판 : 
      </div>
      {boardArray.map((board, index) => (
        <div key={board.boardId}>
          <div>
            {board.boardName}
          </div>
        </div>
      ))}
      <div>
        {
          isFormOpen ?
          <SideForm setIsFormOpen={setIsFormOpen}/>
          :
          <FiPlusCircle onClick={() => setIsFormOpen(!isFormOpen)}/>
        }

      </div>
    </div>
  )
}

export default BoardList
//boardList.css.ts
import { style } from "@vanilla-extract/css";
import { vars } from "../../App.css";

export const container = style({
    display: 'flex',
    flexDirection: 'row',
    alignItems: 'center',
    flexWrap: 'wrap',
    rowGap: 15,
    minHeight: 'max-content',
    padding : vars.spacing.big2,
    backgroundColor: vars.color.mainDarker
})

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

export const addButton = style({
    color: vars.color.brightText,
    fontSize: vars.fontSizing.T2,
    cursor:'pointer',
    marginLeft: vars.spacing.big1,
    ":hover": {
        opacity: 0.8
    }
})

export const boardItem = style({
    color: vars.color.brightText,
    fontSize: vars.fontSizing.T3,
    backgroundColor: vars.color.mainFaded,
    padding: vars.spacing.medium,
    borderRadius: 10,
    cursor:'pointer',
    marginRight: vars.spacing.big1,
    ":hover": {
        opacity: 0.8,
        transform: "scale(1.03)"
    }
})

export const boardItemActive = style({
    color: vars.color.brightText,
    fontSize: vars.fontSizing.T3,
    backgroundColor: vars.color.selectedTab,
    padding: vars.spacing.medium,
    borderRadius: 10,
    cursor: 'pointer',
    marginRight: vars.spacing.big1
})

export const addSection = style ({
    display: 'flex',
    alignItems: 'center',
    marginLeft: 'auto'
})

export const smallTitle = style({
   color: vars.color.brightText,
   fontSize: vars.fontSizing.T3
})

SideForm 생성

//SideForm.tsx
import React, { ChangeEvent, FC, useState } from 'react'
import { FiCheck } from 'react-icons/fi';
import { icon, input, sideForm } from './SideForm.css';
import { useTypedDispatch } from '../../../hooks/redux';
import { v4 as uuidv4} from 'uuid';
import { addBoard } from '../../../store/slices/boardSlice';
import { addLog } from '../../../store/slices/loggerSlice';

type TSideFormProps = {
  inputRef: React.RefObject<HTMLInputElement>,
  setIsFormOpen:  React.Dispatch<React.SetStateAction<boolean>>
}

const SideForm: FC<TSideFormProps> = ({ //타입 설정
  setIsFormOpen, //Props로 가져와 준다.
}) => {
  const [inputText, setInputText] = useState('');
  const dispatch = useTypedDispatch();
  const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
    setInputText(e.target.value);
  }
  const handleOnBlur = () => {
    setIsFormOpen(false);
  }

  const handleClick = () => {
    if(inputText) {
      dispatch(
        addBoard({
          board: {
            boardId: uuidv4(), 
            boardName: inputText,
            lists: []
          }})
      )

      dispatch(
        addLog({
          logId: uuidv4(),
          logMessage: `게시판 등록: ${inputText}`,
          logAuthor: 'User',
          logTimestamp: String(Date.now()),
        })
      )
    }
  }

  return (
    <div className={sideForm}>
      <input 
        // ref={inputRef}
        autoFocus
        className={input}
        type='text'
        placeholder='새로운 게시판 등록하기'
        value={inputText}
        onChange={handleChange}
        onBlur={handleOnBlur}
      />
      <FiCheck className={icon} onMouseDown={handleClick}/>
    </div>
  )
}

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

export const sideForm = style({
    display: 'flex',
    alignItems: 'center',
    marginLeft: 'auto'
})

export const input = style({
    padding: vars.spacing.small,
    fontSize: vars.fontSizing.T4,
    minHeight: 30
})

export const icon = style({
    color: vars.color.brightText,
    fontSize: vars.fontSizing.T2,
    marginLeft: vars.spacing.medium,
    cursor: 'pointer',
    ":hover":{
        opacity: 0.8
    }
})

Action 상태 부여를 위해 BoardSlice와 LoggerSlice에 코드를 추가해줍니다.

const boardsSlice = createSlice({
    name: 'boards',
    initialState,
    reducers: {
        addBoard: (state, { payload }: PayloadAction<TAddBoardAction>) => {
            state.boardArray.push(payload.board);
        }
    }
})

export const { addBoard } = boardsSlice.actions;

```typescript
const loggerSlice = createSlice({
    name: 'logger',
    initialState,
    reducers: {
        addLog: (state, {payload}: PayloadAction<ILogItem>) => {
            state.logArray.push(payload);
        }
    }
})

export const {addLog} = loggerSlice.actions;

list

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'

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 (
    <div
      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}
                taskDecription = {task.taskDescription}
                boardId = {boardId}
                id = {task.taskId}
                index = {index}
              />
            </div>
        ))}
         <ActionButton /> {/*Pops로 분기처리*/}
    </div> 
  )
}

export default List
import { style } from "@vanilla-extract/css";
import { vars } from "../../App.css";

export const listWrapper = style({
    display: 'flex',
    flexDirection: 'column',
    marginRight: vars.spacing.listSpacing,
    padding: vars.spacing.big2,
    minWidth: vars.minWidth.list,
    width: 'max-content',
    height: 'max-content',
    borderRadius: 10,
    backgroundColor: vars.color.list
})

export const name = style({
    fontSize: vars.fontSizing.T3,
    marginBottom: vars.spacing.big2
})

export const header = style({
    display: 'flex',
    alignItems: 'center'
})

export const deleteButton = style({
    padding: vars.spacing.small,
    borderRadius: 20,
    fontSize: vars.fontSizing.T2,
    marginLeft: 'auto',
    marginTop: '-15px',
    marginRight: '5px',
    cursor: 'pointer',
    ':hover': {
        backgroundColor: vars.color.task,
        boxShadow: vars.shadow.basic,
        opacity: 0.8
    }
})
import React, { FC } from 'react'
import { IList } from '../../types';
import List from '../List/List';
import ActionButton from '../ActionButton/ActionButton';
import { listsContainer } from './ListsContainer.css';

type TListsContainerProps = {
  boardId: string;
  lists: IList[];
}

const ListsContainer: FC<TListsContainerProps> = ({
  lists,
  boardId
}) => {
  return (
    <div className={listsContainer}>
      {
        lists.map(list => (
          <List key = {list.listId} list={list} boardId={boardId}/>
        ))
      }
      <ActionButton />
    </div>
  )
}

export default ListsContainer
import { style } from "@vanilla-extract/css";
import { vars } from "../../App.css";

export const listsContainer = style({
    height: 'max-content',
    display: 'flex',
    flexWrap: 'wrap',
    rowGap: vars.spacing.listSpacing,
    margin: vars.spacing.listSpacing
})

task

import React, { FC } from 'react'
import { container, description, title } from './Task.css';

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

const Task: FC<TTaskProps> = ({
  index,
  id,
  boardId,
  taskName,
  taskDescription
}) => {
  return (
    <div className={container}>
      <div className={title}>{taskName}</div>
      <div className={description}>{taskDescription}</div>
    </div>
  )
}

export default Task
//task.css.ts
import { style } from "@vanilla-extract/css";
import { vars } from "../../App.css";

export const container = style ({
    display: 'flex',
    flexDirection: 'column',
    padding: vars.spacing.medium,
    backgroundColor: vars.color.task,
    borderRadius: 10,
    marginBottom: vars.spacing.big2,
    boxShadow: vars.shadow.basic,
    cursor: 'pointer',
    ':hover': {
        backgroundColor: vars.color.taskHover,
        transform: 'scale(1.03)'
    }
})

export const title = style ({
    fontSize: vars.fontSizing.T4,
    fontWeight: 'bold',
    marginBottom: vars.spacing.small
})

export const description = style ({
    fontSize: vars.fontSizing.P1
})

DropDownForm

import React, { ChangeEvent, FC, useState } from 'react'
import { FiX } from 'react-icons/fi';
import { useTypedDispatch } from '../../../hooks/redux';
import { addList, addTask } from '../../../store/slices/boardSlice';
import { addLog } from '../../../store/slices/loggerSlice';
import { v4 } from 'uuid';
import { button, buttons, close, input, listForm, taskForm } from './DropDownForm.css';

type TDropDownFormProps = {
  boardId: string;
  listId: string;
  setIsFormOpen: React.Dispatch<React.SetStateAction<boolean>>
  list?: boolean;
}

const DropDownForm: FC<TDropDownFormProps> = ({
  boardId,
  list,
  listId,
  setIsFormOpen
}) => {
  const dispatch = useTypedDispatch();
  const [ text, setText] = useState('');
  const formPlaceholder = list ?
  "리스트의 제목을 입력하세요." :
  "일의 제목을 입력하세요."

  const buttonTitle = list ?
  "리스트 추가하기" :
  "일 추가하기"

  const handleTextChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
    setText(e.target.value)
  }
  
  const handleButtonClick = () => {
    if(text) {
      if(list) {
        dispatch(
          addList({
            boardId,
            list: {listId: v4(), listName: text, tasks: []}
          })
        );
        dispatch(
          addLog({
            logId: v4(),
            logMessage: `리스트 생성하기: ${text}`,
            logAuthor: 'User',
            logTimestamp: String(Date.now())
          })
        );
      } else {
        dispatch(
          addTask({
            boardId,
            listId,
            task: {
              taskId: v4(),
              taskName: text,
              taskDescription: '',
              taskOwner: 'User'
            }
          })
        )

        dispatch(
          addLog({
            logId: v4(),
            logMessage: `일 생성하기 ${text}`,
            logAuthor: 'User',
            logTimestamp: String(Date.now())
          })
        )

      }
    }
  }
  return (
    <div className={list ? listForm : taskForm}>
        <textarea
          className={input}
          value={text}
          onChange={handleTextChange}
          autoFocus
          placeholder={formPlaceholder}
          onBlur={() => setIsFormOpen(false)}
        />
      <div className={buttons}>
        <button
          className={button}
          onMouseDown={handleButtonClick}>
          {buttonTitle}
        </button>
        <FiX className={close}/>
      </div>
    </div>
  )
}

export default DropDownForm

list추가와 task추가를 위해 boardSlice에 코드를 추가합니다.

const boardsSlice = createSlice({
    name: 'boards',
    initialState,
    reducers: {
        addBoard: (state, { payload }: PayloadAction<TAddBoardAction>) => {
            state.boardArray.push(payload.board);
        },

        addList: (state, {payload}: PayloadAction<TAddListAction>) => {
            state.boardArray.map(board => 
                board.boardId === payload.boardId
                ? {...board, lists: board.lists.push(payload.list)}
                : board
            )
        },

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


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

export const taskForm = style({
    display:'flex',
    flexDirection: 'column',
    height: 'max-content',
    borderRadius: 4,
    marginTop: vars.spacing.small,
    fontSize: vars.fontSizing.T3,
    padding: vars.spacing.medium
})

export const listForm = style({
    display: 'flex',
    flexDirection: 'column',
    marginRight: vars.spacing.listSpacing,
    padding: vars.spacing.big2,
    width: 'max-content',
    height: 'max-content',
    borderRadius: 20,
    backgroundColor: vars.color.list
})

export const input = style({
    padding: vars.spacing.medium,
    fontSize: vars.fontSizing.P1,
    minHeight: 60,
    marginBottom: vars.spacing.medium,
    border: 'none',
    boxShadow: vars.shadow.basic,
    borderRadius: 4,
    resize: 'none',
    overflow: 'hidden',
    wordWrap: 'break-word'
})

export const button = style({
    width: 150,
    color: vars.color.brightText,
    padding: vars.spacing.medium,
    fontSize: vars.fontSizing.T3,
    backgroundColor: vars.color.mainDarker,
    border: 'none',
    cursor: 'pointer',
    ":hover": {
        backgroundColor: vars.color.mainFaded
    }
});

export const buttons = style({
    display: 'flex',
    flexDirection: 'row',
    alignItems: 'center'
})

export const close = style({
    marginLeft: vars.spacing.big2,
    fontSize: vars.fontSizing.T1,
    opacity: 0.5,
    ':hover': {
        opacity: 0.7
    }
})
profile
늒네입니다.

0개의 댓글