본문 바로가기

카테고리 없음

SingleProject_240530 리액트 가계부 RTK 개선

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 month = state.monthData.find((month) => month.id === monthId);
      if (month) {
        month.texts = [...month.texts.filter((t) => t.id !== text.id), text];
      }
    },

    deletedMonthData: (state, action) => {
      const { monthId, textId } = action.payload;
      const month = state.monthData.find((month) => month.id === monthId);
      if (month) {
        month.texts = month.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 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 }));
      });
    });
  }, []);

  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 { 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("");

  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("정말로 삭제하시겠습니까?")) {
      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));
      dispatch(deletedMonthData({ monthId: selectedMonth, textId: id }));
      navigate("/", { state: { selectedMonth } });
    }
  };

  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]);

  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;

 

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: "",
  });

  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: "",
      });
    }
  };

  const handleChange = (e) => {
    const { name, value } = e.target;
    setFormData((prevData) => ({
      ...prevData,
      [name]: value,
    }));
  };

  useEffect(() => {
    setFormData((prevData) => ({
      ...prevData,
      date: getDefaultDate(selectedMonth),
    }));
  }, [selectedMonth]);

  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 { 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 = 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 { 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>
      <ul>
        {selectedTexts.length > 0 ? (
          selectedTexts.map((text) => (
            <li key={text.id}>
              <Link to={`/detail/${text.id}`}>
                {text.date} <br />
                {text.item} - {text.description} {text.amount}
              </Link>
            </li>
          ))
        ) : (
          <p>텍스트를 입력해주세요.</p>
        )}
      </ul>
    </StyledTextBox>
  );
}

export default TextBox;