Board List 생성
//BoardList.tsx
import React, { FC, useState } from 'react'
import { useTypedSelector } from '../../hooks/redux';
import { FiPlusCircle } from 'react-icons/fi';
import SideForm from './SideForm/SideForm';
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>
<div>
게시판 :
</div>
{boardArray.map((board, index) => (
<div key={board.boardId}>
<div>
{board.boardName}
</div>
</div>
))}
<div>
{
isFormOpen ?
<SideForm setIsFormOpen={setIsFormOpen}/>
:
<FiPlusCircle onClick={() => setIsFormOpen(!isFormOpen)}/>
}
</div>
</div>
)
}
export default 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 생성
//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/boardSlice';
import { addLog } from '../../../store/slices/loggerSlice';
type TSideFormProps = {
inputRef: React.RefObject<HTMLInputElement>,
setIsFormOpen: React.Dispatch<React.SetStateAction<boolean>>
}
const SideForm: FC<TSideFormProps> = ({ //타입 설정
setIsFormOpen, //Props로 가져와 준다.
}) => {
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>
)
}
export default SideForm
//SideForm.css.ts
import { style } from "@vanilla-extract/css";
import { vars } from "../../../App.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: 'pointer',
":hover":{
opacity: 0.8
}
})
Action 상태 부여를 위해 BoardSlice와 LoggerSlice에 코드를 추가해줍니다.
const boardsSlice = createSlice({ name: 'boards', initialState, reducers: { addBoard: (state, { payload }: PayloadAction<TAddBoardAction>) => { state.boardArray.push(payload.board); } } })
export const { addBoard } = boardsSlice.actions;
```typescript
const loggerSlice = createSlice({
name: 'logger',
initialState,
reducers: {
addLog: (state, {payload}: PayloadAction<ILogItem>) => {
state.logArray.push(payload);
}
}
})
export const {addLog} = loggerSlice.actions;
list
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/boardSlice'
import { addLog } from '../../store/slices/loggerSlice'
import { v4 } 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: v4(),
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}
taskDecription = {task.taskDescription}
boardId = {boardId}
id = {task.taskId}
index = {index}
/>
</div>
))}
<ActionButton /> {/*Pops로 분기처리*/}
</div>
)
}
export default List
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
}
})
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 TListsContainerProps = {
boardId: string;
lists: IList[];
}
const ListsContainer: FC<TListsContainerProps> = ({
lists,
boardId
}) => {
return (
<div className={listsContainer}>
{
lists.map(list => (
<List key = {list.listId} list={list} boardId={boardId}/>
))
}
<ActionButton />
</div>
)
}
export default ListsContainer
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
})
task
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
//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
})
DropDownForm
import React, { ChangeEvent, FC, useState } from 'react'
import { FiX } from 'react-icons/fi';
import { useTypedDispatch } from '../../../hooks/redux';
import { addList, addTask } from '../../../store/slices/boardSlice';
import { addLog } from '../../../store/slices/loggerSlice';
import { v4 } from 'uuid';
import { button, buttons, close, input, listForm, taskForm } from './DropDownForm.css';
type TDropDownFormProps = {
boardId: string;
listId: string;
setIsFormOpen: React.Dispatch<React.SetStateAction<boolean>>
list?: boolean;
}
const DropDownForm: FC<TDropDownFormProps> = ({
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: v4(), listName: text, tasks: []}
})
);
dispatch(
addLog({
logId: v4(),
logMessage: `리스트 생성하기: ${text}`,
logAuthor: 'User',
logTimestamp: String(Date.now())
})
);
} else {
dispatch(
addTask({
boardId,
listId,
task: {
taskId: v4(),
taskName: text,
taskDescription: '',
taskOwner: 'User'
}
})
)
dispatch(
addLog({
logId: v4(),
logMessage: `일 생성하기 ${text}`,
logAuthor: 'User',
logTimestamp: String(Date.now())
})
)
}
}
}
return (
<div className={list ? listForm : taskForm}>
<textarea
className={input}
value={text}
onChange={handleTextChange}
autoFocus
placeholder={formPlaceholder}
onBlur={() => setIsFormOpen(false)}
/>
<div className={buttons}>
<button
className={button}
onMouseDown={handleButtonClick}>
{buttonTitle}
</button>
<FiX className={close}/>
</div>
</div>
)
}
export default DropDownForm
list추가와 task추가를 위해 boardSlice에 코드를 추가합니다.
const boardsSlice = createSlice({
name: 'boards',
initialState,
reducers: {
addBoard: (state, { payload }: PayloadAction<TAddBoardAction>) => {
state.boardArray.push(payload.board);
},
addList: (state, {payload}: PayloadAction<TAddListAction>) => {
state.boardArray.map(board =>
board.boardId === payload.boardId
? {...board, lists: 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
)
},
```typescript
//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
}
})