[프로그래머스] 프론트엔드 심화: 상태 관리와 비동기 처리(2)

Lina Hongbi Ko·2024년 11월 5일
0

Programmers_BootCamp

목록 보기
49/76
post-thumbnail

2024년 11월 5일

✏️ BoardList

// App.tsx


import { useState } from 'react'
import { appContainer, board, buttons } from './App.css'
import BoardList from './components/BoardList/BoardList'


function App() {
  const [activeBoardId, setActiveBoardId] = useState('board-0');
  return (
    <div className={appContainer}>
      <BoardList activeBoardId={activeBoardId} setActiveBoardId={setActiveBoardId}/>
      <div className={board}>
      
      </div>
      <div className={buttons}>
          <button>
            이 게시판 삭제하기
          </button>
          <button>

          </button>
      </div>
    </div>
  )
}

export default App
// components / BoardList / BoardList.tsx

import React, { FC, useState } from 'react'
import { useTypedSelector } from '../../hooks/redux';
import { FiPlusCircle } from 'react-icons/fi';
import SideForm from './SideForm/SideForm';
import { addButton, addSection, boardItem, boardItemActive, container, title } from './BoardList.css';
import clsx from 'clsx';

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

export default BoardList
// components / 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

// components / BoardList / 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/boardsSlice';
import { addLog } from '../../../store/slices/loggerSlice';

type TSideFormProps = {
  setIsFormOpen : React.Dispatch<React.SetStateAction<boolean>>,
  // inputRef : React.RefObject<HTMLInputElement>
}
const SideForm : FC<TSideFormProps> = ({
  setIsFormOpen,
  // inputRef
}) => {
  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>
    // onClick 을 사용하면 blur와 순서가 겹치기 대문에 mousedown을 이벤트 프로퍼티로 넣는다.
    // blur onmousedown => mouseup => click
  )
}

export default SideForm
// store / slices / boardSlice.ts

... 생략 ,,,

type TAddBoardAction = {
  board : IBoard;
}
... 생략 ...

const boardsSlice = createSlice({
  name: 'boards',
  initialState,
  reducers : {
    addBoard : (state, {payload} : PayloadAction<TAddBoardAction>) => {
      state.boardArray.push(payload.board); // immer라는 라이브러리 사용해서 불변성 지키지 않아도됨
    }
  }
})

export const {addBoard} = boardsSlice.actions;
export const boardsReducer = boardsSlice.reducer;
// store / slies / loggerSlciees.ts

... 생략 ...

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

export const { addLog } = loggerSlice.actions;
export const loggerReducer = loggerSlice.reducer;
// components / BoardList / SideForm / SideForm.css.ts

import { vars } from '../../../App.css';
import { style } from '@vanilla-extract/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 : "pinter",
  ":hover" : {
    opacity: 0.8
  }
})

✏️ ListContainer

// App.tsx

import { useState } from 'react'
import { appContainer, board, buttons } from './App.css'
import BoardList from './components/BoardList/BoardList'
import ListsContainer from './components/ListsContainer/ListsContainer';
import { useTypedSelector } from './hooks/redux';


function App() {
  const [activeBoardId, setActiveBoardId] = useState('board-0');
  const boards = useTypedSelector(state => state.boards.boardArray);
  const getActiveBoard = boards.filter(board => board.boardId === activeBoardId)[0];
  const lists = getActiveBoard.lists;
  return (
    <div className={appContainer}>
      <BoardList activeBoardId={activeBoardId} setActiveBoardId={setActiveBoardId}/>
      <div className={board}>
        <ListsContainer lists={lists} boardId={getActiveBoard.boardId}/>
      </div>
      <div className={buttons}>
          <button>
            이 게시판 삭제하기
          </button>
          <button>

          </button>
      </div>
    </div>
  )
}

export default App
// components / ListContainer / ListContainer.tsx

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 TListContainerProps = {
  boardId : string;
  lists : IList[];
}
const ListsContainer: FC<TListContainerProps>= ({
  lists,
  boardId
}) => {
  return (
    <div className={listsContainer}>
      {
        lists.map(list => (
          <List key={list.listId} list={list} boardId={boardId}/>
        ))
      }
      <ActionButton />
    </div>
  )
}

export default ListsContainer
// components / List / List.tsx

import React from 'react'

const List = ({
  list,
  boardId
}) => {
  return (
    <div>List</div>
  )
}

export default List
// components / ListContainer / ListContainer.css.ts

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
})

✏️ List

// components / List / 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/boardsSlice'
import { addLog } from '../../store/slices/loggerSlice'
import { v4 as uuidv4} 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: uuidv4(),
        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}
            taskDescription={task.taskDescription}
            boardId={boardId}
            id={task.taskId}
            index={index}
          />
        </div>))
      }
      <ActionButton />
    </div>
  )
}

export default List
// store / slices / boardsSlice.ts

... 생략 ...

type TDeleteListAction = {
  boardId: string;
  listId: string;
}

... 생략 ,,,

const boardsSlice = createSlice({
  name: 'boards',
  initialState,
  reducers : {
    addBoard : (state, {payload} : PayloadAction<TAddBoardAction>) => {
      state.boardArray.push(payload.board); // immer라는 라이브러리 사용해서 불변성 지키지 않아도됨
    },
    deleteList : (state, {payload} : PayloadAction<TDeleteListAction>) => {
      state.boardArray = state.boardArray.map(
        board => board.boardId === payload.boardId ? {...board, lists : board.lists.filter(
          list => list.listId !== payload.listId
        )
      }
      :
      board
      )
    },
    setModalActive : (state, {payload}: PayloadAction<boolean>) => {
      state.modalActive = payload;
    }
  }
})

export const {addBoard, deleteList, setModalActive} = boardsSlice.actions;
export const boardsReducer = boardsSlice.reducer;
// store / slices / modalSice.ts

import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { ITask } from '../../types';

type TModalState = {
  boardId: string;
  listId: string;
  task: ITask;
}

type TSetModalDataAction = {
  boardId : string;
  listId : string;
  task: ITask;
}

const initialState : TModalState = {
  boardId: "board-0",
  listId: "list-0",
  task: {
    taskId: "task-0",
    taskName: "task 0",
    taskDescription: "task description",
    taskOwner: "Lina"
  }
};

const modalSlice = createSlice({
  name: 'modal',
  initialState,
  reducers: {
    setModalData : (state, {payload} : PayloadAction<TSetModalDataAction>) => {
      state.boardId = payload.boardId;
      state.listId = payload.listId;
      state.task = payload.task;
    }
  }

});
export const { setModalData } = modalSlice.actions;
export const modalReducer = modalSlice.reducer;
// components / List / List.css.ts

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
  }
})

✏️ Task

// components / Task / Task.tsx

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
// components / 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
})

✏️ ActionButton

// components / List / List.tsx

... 생략 ...

<ActionButton
        boardId={boardId}
        listId={list.listId}
      />
    </div>
  )
}

export default List
// components / ListContainer / ListContainer.tsx

... 생략 ...

<ActionButton
        boardId={boardId}
        listId={""}
        list
      />
    </div>
  )
}

export default ListsContainer
// components / ActionButton / ActionButton.tsx

import React, { FC, useState } from 'react'
import DropDownForm from './DropDownForm/DropDownForm';
import { IoIosAdd } from 'react-icons/io';
import { listButton, taskButton } from './ActionButton.css';

type TActionButtonProps = {
  boardId: string;
  listId: string;
  list?: boolean;
}
const ActionButton: FC<TActionButtonProps> = ({
  boardId,
  listId,
  list
}) => {
  const [isFormOpen, setIsFormOpen] = useState(false);
  const buttonText = list ? "새로운 리스트 등록" : "새로운 일 등록";

  return isFormOpen ? (
    <DropDownForm
      setIsFormOpen={setIsFormOpen}
      list={list ? true : false}
      listId={listId}
      boardId={boardId}
    />
  ) 
  :
  (<div
      className={list ? listButton : taskButton}
      onClick={()=> setIsFormOpen(true)}
    >
    <IoIosAdd />
    <p>{buttonText}</p>
  </div>)
}

export default ActionButton
// components / ActionButton / ActionButton.css.ts

import { style } from '@vanilla-extract/css';
import { vars } from '../../App.css';

export const taskButton = style({
  display : "flex",
  alignItems : "center",
  height : "max-content",
  borderRadius : 4,
  marginTop : vars.spacing.small,
  fontSize : vars.fontSizing.T3,
  padding : vars.spacing.medium,
  cursor : "pointer",
  ":hover" : {
    backgroundColor : vars.color.secondaryDarkTextHover
  }
})

export const listButton = style({
  display : "flex",
  alignItems : "center",
  height : "max-content",
  borderRadius : 4,
  minWidth : vars.minWidth.list,
  marginTop : vars.spacing.small,
  color : vars.color.brightText,
  fontSize : vars.fontSizing.T3,
  backgroundColor : vars.color.mainFaded,
  paddingTop : vars.spacing.small,
  paddingBottom : vars.spacing.small,
  paddingRight : vars.spacing.big2,
  paddingLeft : vars.spacing.big2,
  cursor : "pointer",
  ":hover" : {
    backgroundColor : vars.color.mainFadedBright
  }
})

✏️ DropDownForm

// components / DropDownForm / DropDownForm.tsx

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

type TDropDownFromProps = {
  boardId : string;
  listId : string;
  setIsFormOpen : React.Dispatch<React.SetStateAction<boolean>>;
  list?: boolean;
}
const DropDownForm: FC<TDropDownFromProps> = ({
  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: uuidv4(), listName: text, tasks: []}
          })
        );

        dispatch(
          addLog({
            logId: uuidv4(),
            logMessage : `리스트 생성하기: ${text}`,
            logAuthor : "User",
            logTimeStamp : String(Date.now())
          })
        )
      } else {
        dispatch(
          addTask({
            boardId,
            listId,
            task: {
              taskId : uuidv4(),
              taskName : text,
              taskDescription : "",
              taskOwner : "User"
            }
          })
        );

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

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

export default DropDownForm
// store / slices / boardsSlice.ts

... 생략 ...

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

type TAddTaskAction = {
  boardId: string;
  listId: string;
  task: ITask;
}

... 생략 ...

const boardsSlice = createSlice({
  name: 'boards',
  initialState,
  reducers : {
    ... 생략 ...
    addList: (state, {payload}: PayloadAction<TAddListAction>) => {
      state.boardArray.map( board => board.boardId === payload.boardId ?
        {...board, list: 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
      )
    },
    ... 생략 ...

export const {addBoard, deleteList, setModalActive, addList, addTask} = boardsSlice.actions;
export const boardsReducer = boardsSlice.reducer;
// components / DropDownForm / 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개의 댓글