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

Lina Hongbi Ko·2024년 11월 6일
0

Programmers_BootCamp

목록 보기
50/76
post-thumbnail

2024년 11월 6일

✏️ ModalEdit

// 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';
import EditModal from './components/EditModal/EditModal';


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;
  const modalActive = useTypedSelector(state => state.boards.modalActive);

  return (
    <div className={appContainer}>
      {
        modalActive ? <EditModal /> : null
      }
      ... 생략 ...
// components / EditModal / 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/boardsSlice';
import { addLog } from '../../store/slices/loggerSlice';
import { v4 as uuidv4 } from 'uuid';
import { buttons, deleteButton, header, input, modalWindow, title, updateButton, wrapper } from './EditModal.css';

const EditModal = () => {
  const editingState = useTypedSelector(state => state.modal);
  const [data, setData] = useState(editingState);
  const dispatch = useTypedDispatch();
  
  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 : uuidv4(),
        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 : uuidv4(),
        logMessage : `일 삭제하기 : ${editingState.task.taskName}`,
        logAuthor : "User",
        logTimeStamp : String(Date.now())
      })
    );

    dispatch(
      setModalActive(false)
    )
  }
  return (
    <div className={wrapper}>
      <div className={modalWindow}>
        <div className={header}>
          <div className={title}>{editingState.task.taskName}</div>
          <FiX className={closeButton} onClick={handleCloseButton}/>
        </div>
        <div className={title}>제목</div>
        <input className={input} type="text" value={data.task.taskName} onChange={handleNameChange}/>
        <div className={title}>설명</div>
        <input className={input} type='text' value={data.task.taskDescription} onChange={handleDescriptionChange}/>
        <div className={title}>생성한 사람</div>
        <input className={input} type='text' value={data.task.taskOwner} onChange={handleAuthorChange} />
        <div className={buttons}>
          <button className={updateButton} onClick={handleUpdate}>일 수정하기</button>
          <button className={deleteButton} onClick={handleDelete}>일 삭제하기</button>
        </div>
      </div>
    </div>
  )
}

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

... 생략 ...

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

type TDeleteTaskAction = {
  boardId: string;
  listId: string;
  taskId: string;
}

... 생략 ...

const boardsSlice = createSlice({
  name: 'boards',
  initialState,
  reducers : {
    ... 생략 ...
    
    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
      )
    },
... 생략 ...

export const {addBoard, deleteList, setModalActive, addList, addTask, updateTask, deleteTask} = boardsSlice.actions;
export const boardsReducer = boardsSlice.reducer;
// components / EditModal / 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 : "-2Opx",
  ":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,
  ":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

// App.tsx

... 생략 ...
 
import LoggerModal from './components/LoggerModal/LoggerModal';

function App() {
  const [isLoggerOpen, setIsLoggerOpen] = useState(false);
  ... 생략 ...
  
  return (
    <div className={appContainer}>
      {
        isLoggerOpen ? <LoggerModal setIsLoggerOpen={setIsLoggerOpen}/> : null
      }
      
      ... 생략 ...
// App.css.ts

... 생략 ...

export const deleteBoardButton = style({
  border : "none",
  borderRadius : 5,
  width : "max-content",
  marginTop : "auto",
  marginLeft : "auto",
  marginBottom : 30,
  fontSize : vars.fontSizing.T4,
  padding : vars.spacing.big2,
  backgroundColor : vars.color.mainFaded,
  cursor : "pointer",
  opacity : 0.6,
  minWidth: 150,
  ":hover" : {
    opacity : 0.8
  }
})

export const loggerButton = style({
  border : "none",
  borderRadius : 5,
  width : "max-content",
  marginTop : "auto",
  marginLeft : "15px",
  marginRight : "30px",
  marginBottom : "30px",
  fontSize : vars.fontSizing.T4,
  padding : vars.spacing.big2,
  backgroundColor : vars.color.mainFaded,
  cursor : "pointer",
  opacity : 0.6,
  minWidth : 150,
  ":hover" : {
    opacity: 0.8
  }
})
// components / loggerModal / loggerModal.tsx

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
// components / loggerModal / 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",
  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 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 : "-2Opx",
  ":hover" : {
    opacity: 0.8
  }
})

export const body = style({
  maxHeight : "400px",
  overflowY : "auto",
  width : "100%"
})

✏️ LogItem

import React, { FC } from 'react'
import { ILogItem } from '../../../types'
import { BsFillPersonFill } from 'react-icons/bs'
import { author, date, logItemWrap, message } from './LogItem.css';

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 ? `just now` : ""}
  `
  return (
    <div className={logItemWrap}>
      <div className={author}>
        <BsFillPersonFill />
        {logItem.logAuthor}
      </div>
      <div className={message}>{logItem.logMessage}</div>
      <div className={date}>{showOffsetTime}</div>
    </div>
  )
}

export default LogItem
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 rgb(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
})

✏️ 게시판 삭제 기능 생성

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


function App() {
  const [isLoggerOpen, setIsLoggerOpen] = useState(false);
  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;
  const modalActive = useTypedSelector(state => state.boards.modalActive);

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

      const newIndexToset = () => {
        const indexToBeDeleted = boards.findIndex(board => board.boardId === activeBoardId);
        return indexToBeDeleted === 0 ? indexToBeDeleted + 1 : indexToBeDeleted - 1;
      }
	    
	    setActiveBoardId(boards[newIndexToset()].boardId)

    } else {
      alert('최소 게시판 개수는 한 개입니다.');
    }
  }

  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
// store / slice /boardSlice.ts

... 생략 ...
type TDeleteBoardAction = {
  boardId: string;
}


... 생략 ...

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

export const {addBoard, deleteBoard, deleteList, setModalActive, addList, addTask, updateTask, deleteTask} = boardsSlice.actions;
export const boardsReducer = boardsSlice.reducer;
profile
프론트엔드개발자가 되고 싶어서 열심히 땅굴 파는 자

0개의 댓글