juni
SingleProject_리액트 가계부 redux 본문
📦src
┣ 📂assets
┃ ┗ 📜react.svg
┣ 📂components
┃ ┣ 📜AccountBookForm.jsx
┃ ┣ 📜Box.jsx
┃ ┣ 📜BoxContainer.jsx
┃ ┣ 📜GlobalStyle.jsx
┃ ┗ 📜TextBox.jsx
┣ 📂context
┃ ┗ 📜AccountBookContext.jsx
┣ 📂pages
┃ ┣ 📜Detail.jsx
┃ ┗ 📜Home.jsx
┣ 📂redux
┃ ┣ 📂config
┃ ┃ ┗ 📜configStore.js
┃ ┗ 📂slices
┃ ┃ ┗ 📜accountBookSlice.js
┣ 📜App.jsx
┗ 📜main.jsx
import { createSlice } from "@reduxjs/toolkit";
const initMonthData = Array.from({ length: 12 }, (_, i) => ({
id: i + 1,
month: `${i + 1}월`,
texts: [],
}));
const initialState = {
monthData: initMonthData,
selectedMonth: 1,
};
const accountBookSlice = createSlice({
initialState,
name: "accountBook",
reducers: {
updatedMonthData: (state, action) => {
const { monthId, text } = action.payload;
const monthIndex = state.monthData.findIndex(
(month) => month.id === monthId
);
if (monthIndex !== -1) {
const textIndex = state.monthData[monthIndex].texts.findIndex(
(t) => t.id === text.id
);
if (textIndex !== -1) {
state.monthData[monthIndex].texts[textIndex] = text;
} else {
state.monthData[monthIndex].texts.push(text);
}
}
},
deletedMonthData: (state, action) => {
const { monthId, textId } = action.payload;
const monthIndex = state.monthData.findIndex(
(month) => month.id === monthId
);
if (monthIndex !== -1) {
state.monthData[monthIndex].texts = state.monthData[
monthIndex
].texts.filter((t) => t.id !== textId);
}
},
updatedMonth: (state, action) => {
state.selectedMonth = action.payload;
},
},
});
export const { updatedMonthData, deletedMonthData, updatedMonth } =
accountBookSlice.actions;
export default accountBookSlice.reducer;
export { initMonthData };
import { BrowserRouter, Route, Routes } from "react-router-dom";
import Home from "./pages/Home";
import Detail from "./pages/Detail";
import GlobalStyle from "./components/GlobalStyle.jsx";
import { Provider } from "react-redux";
import store from "./redux/config/configStore";
// import { AccountBookProvider } from "./context/AccountBookContext.jsx";
function App() {
return (
// <AccountBookProvider>
<Provider store={store}>
<BrowserRouter>
<GlobalStyle />
<Routes>
<Route path="/" element={<Home />} />
<Route path="/detail/:id" element={<Detail />} />
</Routes>
</BrowserRouter>
</Provider>
// </AccountBookProvider>
);
}
export default App;
import BoxContainer from "../components/BoxContainer";
import AccountBookForm from "../components/AccountBookForm";
import { useDispatch, useSelector } from "react-redux";
import { useEffect } from "react";
import {
updatedMonthData,
initMonthData,
} from "../redux/slices/accountBookSlice";
const Home = () => {
const monthData = useSelector((state) => state.AccountBook.monthData);
const selectedMonth = useSelector((state) => state.AccountBook.selectedMonth);
const dispatch = useDispatch();
useEffect(() => {
const storedMonthData =
JSON.parse(localStorage.getItem("monthData")) || initMonthData;
storedMonthData.forEach((month) => {
month.texts.forEach((text) => {
dispatch(updatedMonthData({ monthId: month.id, text }));
});
});
}, [dispatch]);
useEffect(() => {
if (monthData !== initMonthData) {
localStorage.setItem("monthData", JSON.stringify(monthData));
}
}, [monthData]);
useEffect(() => {
localStorage.setItem("selectedMonth", JSON.stringify(selectedMonth));
}, [selectedMonth]);
return (
<div>
<h1>Home</h1>
<AccountBookForm />
<BoxContainer />
</div>
);
};
export default Home;
import styled from "styled-components";
import { useState, useEffect } from "react";
import { v4 as uuidv4 } from "uuid";
import { useDispatch, useSelector } from "react-redux";
import { updatedMonthData } from "../redux/slices/accountBookSlice";
const FormWrapper = styled.div`
display: flex;
flex-wrap: wrap;
gap: 10px;
`;
const AccountBookForm = () => {
const selectedMonth = useSelector((state) => state.AccountBook.selectedMonth);
const dispatch = useDispatch();
const onAdd = (text) => {
dispatch(updatedMonthData({ monthId: selectedMonth, text }));
};
const getDefaultDate = (month) => {
const year = new Date().getFullYear();
const monthString = month < 10 ? `0${month}` : month;
return `${year}-${monthString}-01`;
};
const [formData, setFormData] = useState({
date: getDefaultDate(selectedMonth),
item: "",
amount: "",
description: "",
});
useEffect(() => {
setFormData((prevData) => ({
...prevData,
date: getDefaultDate(selectedMonth),
}));
}, [selectedMonth]);
const handleChange = (e) => {
const { name, value } = e.target;
setFormData((prevData) => ({
...prevData,
[name]: value,
}));
};
const validateForm = () => {
const { date, item, amount, description } = formData;
const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
const amountRegex = /^\d+(\.\d{1,2})?$/;
if (!date || !item || !amount || !description) {
alert("빈 칸을 채워주세요.");
return false;
}
if (!dateRegex.test(date)) {
alert("날짜 형식이 올바르지 않습니다. YYYY-MM-DD 형식으로 입력해주세요.");
return false;
}
if (!amountRegex.test(amount)) {
alert("금액에는 숫자를 입력해주세요.");
return false;
}
return true;
};
const handleSubmit = (e) => {
e.preventDefault();
if (validateForm()) {
onAdd({ id: uuidv4(), ...formData });
setFormData({
date: getDefaultDate(selectedMonth),
item: "",
amount: "",
description: "",
});
}
};
return (
<FormWrapper>
<form onSubmit={handleSubmit}>
<input
type="date"
name="date"
value={formData.date}
onChange={handleChange}
placeholder="YYYY-MM-DD"
/>
<input
type="text"
name="item"
value={formData.item}
onChange={handleChange}
placeholder="지출 항목"
/>
<input
type="number"
name="amount"
value={formData.amount}
onChange={handleChange}
placeholder="지출 금액"
/>
<input
type="text"
name="description"
value={formData.description}
onChange={handleChange}
placeholder="지출 내용"
/>
<button type="submit">저장</button>
</form>
</FormWrapper>
);
};
export default AccountBookForm;
import styled from "styled-components";
import Box from "./Box";
import TextBox from "./TextBox";
// import { useContext } from "react";
// import { AccountBookContext } from "../context/AccountBookContext";
import { useSelector } from "react-redux";
const Container = styled.div`
display: flex;
flex-direction: column;
align-items: center;
`;
const BoxesWrapper = styled.div`
display: flex;
flex-wrap: wrap;
gap: 10px;
`;
function BoxContainer() {
// const { monthData } = useContext(AccountBookContext);
const monthData = useSelector((state) => state.AccountBook.monthData);
return (
<Container>
<BoxesWrapper>
{monthData.map((month) => (
<Box key={month.id} id={month.id} month={month.month} />
))}
</BoxesWrapper>
<TextBox />
</Container>
);
}
export default BoxContainer;
import styled from "styled-components";
// import { useContext } from "react";
// import { AccountBookContext } from "../context/AccountBookContext";
import { useDispatch, useSelector } from "react-redux";
import { updatedMonth } from "../redux/slices/accountBookSlice";
const StyledBox = styled.div`
width: 200px;
background-color: ${(props) => (props.selected ? "blue" : "gray")};
color: white;
padding: 10px;
margin: 10px;
cursor: pointer;
text-align: center;
border-radius: 5px;
`;
function Box({ id, month }) {
const dispatch = useDispatch();
const selectedMonth = useSelector((state) => state.AccountBook.selectedMonth);
const isSelected = selectedMonth === id;
const handleClick = () => {
dispatch(updatedMonth(id));
};
return (
<StyledBox onClick={handleClick} selected={isSelected}>
<h3>{month}</h3>
</StyledBox>
);
}
export default Box;
import { Link } from "react-router-dom";
import styled from "styled-components";
import { useSelector } from "react-redux";
const StyledTextBox = styled.div`
width: 100%;
background-color: white;
color: black;
padding: 20px;
margin-top: 20px;
text-align: center;
border-radius: 10px;
li,
p {
background-color: lightgray;
padding: 10px;
border-radius: 10px;
margin-bottom: 20px;
}
`;
function TextBox() {
const monthData = useSelector((state) => state.AccountBook.monthData);
const selectedMonth = useSelector((state) => state.AccountBook.selectedMonth);
const selectedTexts =
monthData.find((month) => month.id === selectedMonth)?.texts || [];
return (
<StyledTextBox>
{selectedTexts.length > 0 ? (
<ul>
{selectedTexts.map((text) => (
<li key={text.id}>
<Link to={`/detail/${text.id}`}>
{text.date} <br />
{text.item} - {text.description} {text.amount}원
</Link>
</li>
))}
</ul>
) : (
<p>텍스트를 입력해주세요.</p>
)}
</StyledTextBox>
);
}
export default TextBox;
import styled from "styled-components";
import { useRef, useEffect } from "react";
import { useParams, useNavigate, Link } from "react-router-dom";
import { useDispatch, useSelector } from "react-redux";
import {
updatedMonthData,
deletedMonthData,
} from "../redux/slices/accountBookSlice";
const StyledTextBox = styled.div`
width: 100%;
background-color: white;
color: black;
padding: 20px;
margin-top: 20px;
text-align: center;
border-radius: 10px;
`;
function Detail() {
const monthData = useSelector((state) => state.AccountBook.monthData);
const selectedMonth = useSelector((state) => state.AccountBook.selectedMonth);
const dispatch = useDispatch();
const { id } = useParams();
const navigate = useNavigate();
const dateRef = useRef("");
const itemRef = useRef("");
const amountRef = useRef("");
const descriptionRef = useRef("");
useEffect(() => {
let foundText = null;
monthData.forEach((month) => {
month.texts.forEach((text) => {
if (text.id === id) {
foundText = text;
}
});
});
if (foundText) {
dateRef.current.value = foundText.date;
itemRef.current.value = foundText.item;
amountRef.current.value = foundText.amount;
descriptionRef.current.value = foundText.description;
}
}, [id, monthData]);
const handleSave = () => {
const updatedText = {
id,
date: dateRef.current.value,
item: itemRef.current.value,
amount: amountRef.current.value,
description: descriptionRef.current.value,
};
dispatch(updatedMonthData({ monthId: selectedMonth, text: updatedText }));
navigate("/", { state: { selectedMonth } });
};
const handleDelete = () => {
if (window.confirm("정말로 삭제하시겠습니까?")) {
dispatch(deletedMonthData({ monthId: selectedMonth, textId: id }));
const updatedMonthData = monthData.map((month) => {
if (month.id === selectedMonth) {
return {
...month,
texts: month.texts.filter((text) => text.id !== id),
};
}
return month;
});
localStorage.setItem("monthData", JSON.stringify(updatedMonthData));
navigate("/", { state: { selectedMonth } });
}
};
return (
<StyledTextBox>
<h1>Detail</h1>
<div>
<input type="date" name="date" ref={dateRef} />
<br />
<input type="text" name="item" ref={itemRef} />
<br />
<input type="number" name="amount" ref={amountRef} />
<br />
<input type="text" name="description" ref={descriptionRef} />
<br />
<button onClick={handleSave}>수정</button>
<button onClick={handleDelete}>삭제</button>
<Link to="/" state={{ selectedMonth }}>
뒤로 가기
</Link>
</div>
</StyledTextBox>
);
}
export default Detail;
'프로젝트 > 미니 프로젝트' 카테고리의 다른 글
TeamProject_뉴스피드 프로젝트 (0) | 2024.06.01 |
---|---|
SingleProject_리액트 가계부 RTK 개선 (0) | 2024.05.30 |
SingleProject_리액트 가계부 context (0) | 2024.05.26 |
SingleProject_가계부(React) (0) | 2024.05.24 |
React 학습 (0) | 2024.05.21 |