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