프로젝트/Task 어플리케이션

1119 EditModal, LoggerModal, LogItem 컴포넌트 생성, 게시판 삭제 기능

thinktank911 2025. 11. 19. 15:37

EditModal 컴포넌트 생성

App.tsx

  • modalActive의 상태 데이터를 리덕스 스토어에서 useSelector로 가져오기
  • modalActive가 true일 때 <EditModal> 컴포넌트 보여주고, 아닐 경우 null
const modalActive = useTypedSelector(state => state.boards.modalActive);

{modalActive ? <EditModal /> : null}

<BoardList 
    activeBoardId={activeBoardId} 
    setActiveBoardId={setActiveBoardId} 
/>

EditModal.tsx

모달 데이터 수정

  • 태스크 클릭 시 태스크에 담긴 데이터를 editModal에 뿌려줘야 하므로 리덕스 스토어에서 modal state를 가져온다.
  • 받아온 데이터를 수정하기 위해 useState 사용 : 초기값으로 스토어에서 가져온 editingState 넣어줌
const editingState = useTypedSelector(state => state.modal);
// state에 넣어주기
const [data, setdata] = useState(editingState);

 

  • EditModal에 input 항목인 '제목, 설명, 생성한 사람'에 각각 onChange 이벤트 건다.
    • handleNameChange에 setData로 editingState 내용 얕은 복사 후 수정
      ➡️ 수정할 부분 e.target.value로 대입
const handleNameChange = (e:ChangeEvent<HTMLInputElement>) => {
// 얕은 복사 : 주소값 복사. 원본도 수정
// Object.assign(), 스프레드 연산자(...)등
    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
      }
    })
}



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

 

일 수정하기 기능 구현

  • handleUpdate 함수 선언
  • dispatch action 전달
    ➡️ 일 수정하기(updateTask), 로그 남기기(addLog), 모달창 닫기(setModalActive)
// EditModal.tsx
const handleUpdate = () => {
// 일 수정하기
dispatch(updateTask({
  boardId: editingState.boardId,	// 스토어 데이터(안바뀌는 부분)
  listId: editingState.listId,		// 스토어 데이터(안바뀌는 부분)
  task: data.task					// useState 데이터(바뀌는 부분)
}));

// 로그 남기기
dispatch(
  addLog({
    logId: uuidv4(),
    logMessage: `일 수정하기: ${editingState.task.taskName}`,
    logAuthor: 'admin',
    logTimestamp: String(Date.now())
  })
)

// 모달창 닫기
dispatch(setModalActive(false));

}

 

일 수정하기(updateTask)

  • boardsSlice.ts 에서 컴포넌트에서 전달한 action.payload 가공
    • boardArray에서 payload로 보내준 boardId값과 일치하는지 확인
    • 해당 board 얕은 복사 ➡️ payload.listId 값과 일치한 list 얕은 복사 ➡️ payload.taskId와 같은 값 찾아서 task 수정
// boardsSlice.ts
type TAddTaskAction = {
    boardId: string;
    listId: string;
    task: ITask;
}

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

 

로그 남기기(addLog)

loggerSlice.ts에서 action.payload를 logArray 배열에 추가

// loggerSlice.ts
reducers: {
    addLog: (state, {payload}: PayloadAction<ILogItem>) => {
        state.logArray.push(payload);
    }
}

 

모달창 닫기(setModalActive)

boardsSlice.ts 에서 컴포넌트에서 전달한 action.payload 가공

setModalActive: (state, {payload} : PayloadAction<boolean>) => {
    state.modalActive = payload;
}

 

 

일 삭제하기 기능 구현

  • 변경 데이터 안 넘겨주고 id만 넘겨주면 된다.
  • dispatch 액션으로 일 삭제하기(deleteTask) 넘겨주기 (나머지는 일 수정하기랑 똑같다.)
// 일 삭제하기
const handleDelete = () => {
    dispatch(deleteTask({
      boardId: editingState.boardId,
      listId: editingState.listId,
      taskId: editingState.task.taskId
    }));

    dispatch(
      addLog({
        logId: uuidv4(),
        logMessage: `일 삭제하기: ${editingState.task.taskName}`,
        logAuthor: 'admin',
        logTimestamp: String(Date.now())
      })
    )

    dispatch(setModalActive(false));
}

 

일 삭제하기(deleteTask)

  • boardsSlice.ts 에서 컴포넌트에서 전달한 action.payload 가공
    • 삭제 시에는 map 대신 filter 함수 사용해 taskId가 payload.taskId랑 다른 것만 남긴다.
// boardsSlice.ts
type TDeleteTaskAction = {
    boardId: string;
    listId: string;
    taskId: string;
}

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

 

모달 입력 폼 닫기 기능 구현

  • 모달의 x버튼 클릭 시 dispatch로 setModalActive(false) 액션 함수 전달
// EditModal.ts
const handleCloseButton = () => {
	dispatch(setModalActive(false));
}

 


LoggerModal 컴포넌트 생성

App.tsx

  • LoggetModal 오픈 여부를 useState로 isLoggerOpen 변수 생성
  • isLoggerOpen 이 true면 <LoggerModal /> 컴포넌트 띄워주고 props로 setIsLoggerOpen 넘겨준다.
  • 활동목록 버튼에 onClick= setIsLoggerOpen(!isLoggerOpen)으로 토글
const [isLoggerOpen, setIsLoggerOpen] = useState(false);

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

<button className={loggerButton} onClick={() => setIsLoggerOpen(!isLoggerOpen)}>
  {isLoggerOpen ? "활동목록 숨기기" : "활동목록 보이기"}
</button>

 

LoggerModal.tsx

  • props로 받은 setIsLoggerOpen에 대한 타입 지정
  • useSelector로 리덕스 스토어에서 logArray 데이터 가져오기
  • 받아온 데이터 logs를 map 돌려서 <LogItem / > 컴포넌트 뿌려주기
    • key 셋팅 잊지 않기
    • logItem 뿌려주기 위한 데이터 props로 넘겨주기
  • X아이콘 버튼에 props로 받아온 모달창 닫는 setIsLoggerOpen함수 이벤트 걸기
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, index) => (
            <LogItem key={log.logId} logItem={log} />
          ))}
        </div>
      </div>
    </div>
  )
}

 

LogItem 컴포넌트 생성

  • props로 받은 logItem에 대한 타입 지정
  • 로그 기록이므로 로그를 남긴 시간이 중요 ➡️ timeOffset 설정
    • 분수가 0보다 크면 분 설정, 초수가 0보다 크면 초 설정
  • 사람 아이콘 : BsFillPersonFill
  • logItem 데이터에서 생성자(logAuthor), 내용(logMessage) 가져와서 뿌리기
  • 로그 시간 기록 : showOffsetTime
type TLogItemProps = {
  logItem: ILogItem;
}

const LogItem : FC<TLogItemProps> = ({
  logItem
}) => {
  // 언제 올렸는지 시간 셋팅하기
  let timeOffset = new Date(Date.now() - Number(logItem.logTimestamp));
  console.log('timeOffset', timeOffset);
  console.log('timeOffset.getMinutes()', timeOffset.getMinutes());
  console.log('timeOffset.getSeconds()', timeOffset.getSeconds());

  const showOffsetTime = `
    ${timeOffset.getMinutes() > 0 ? `${timeOffset.getMinutes()}m` : ""}
    ${timeOffset.getSeconds() > 0 ? `${timeOffset.getSeconds()}s` : ""}
    ${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>
  )
}

게시판 삭제 기능 생성

  • 게시판 1개일 경우 삭제 안 되고 알림창 띄운다.
  • 삭제 시 활성화 인덱스를 변경한다.

App.tsx

게시판 삭제하기 함수 내용

  • 게시판 수가 1이하면 '최소 게시판 개수는 한 개입니다.' 알림창 뜨기
  • 게시판 수가 1 초과면 action 함수 전달
    ➡️ 게시판삭제(deleteBoard), 로그 생성(addLog)
  • 삭제 시 활성화 인덱스 변경
    • findIndex로 getActiveBoard의 boardId를 삭제할 boardId로 정의하고 해당 인덱스 찾기
    • 삭제할 게시판 인덱스가 0이면 다음 인덱스, 0아니면 이전 인덱스 리턴
    • 게시판 활성화 함수 setActiveBoardId에 리턴한 인덱스값 넣어 activeBoardId 재설정
// App.tsx
// 게시판 삭제하기
 const handleDeleteBoard = () => {
    if(boards.length > 1){
      dispatch(deleteBoard({boardId: getActiveBoard.boardId})); // 게시판 삭제

      // 로그 생성
      dispatch(
        addLog({
          logId: uuidv4(),
          logMessage: `게시판 지우기: ${getActiveBoard.boardName}`,
          logAuthor: 'admin',
          logTimestamp: String(Date.now())
        })
      );

      // 삭제 시 활성화 인덱스 변경
      const newIndexToSet = () => {
        // 삭제 인덱스 찾기
        const indexToBeDeleted = boards.findIndex(
                                  board => board.boardId === getActiveBoard.boardId
                                );
        // 삭제 인덱스가 0이면 그 다음, 아니면 그 전 인덱스 리턴
        return indexToBeDeleted === 0 ? indexToBeDeleted + 1 : indexToBeDeleted - 1;
      }

      // 활성화 아이디 셋팅
      setActiveBoardId(boards[newIndexToSet()].boardId);

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

 

게시판 삭제(deleteBoard)

  • boardsSlice.ts 에서 컴포넌트에서 전달한 action.payload 가공
    • 삭제 시에는 map 대신 filter 함수 사용해 taskId가 payload.boardId랑 다른 것만 남긴다.
// boardsSlice.ts
deleteBoard: (state, {payload} : PayloadAction<TDeleteBoardAction>) => {
    state.boardArray = state.boardArray.filter(
        board => board.boardId !== payload.boardId
    )
},

 


리덕스 스토어 활용법과 액션 함수 활용, props로 데이터 주고받는 것에 조금씩 익숙해지고 있다.