2024년 11월 6일
// 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
})
// 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%"
})
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;