본문 바로가기

카테고리 없음

SingleProject_240526 리액트 가계부 context

 

 

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 { AccountBookProvider } from "./context/AccountBookContext.jsx";

function App() {
  return (
    <AccountBookProvider>
      <BrowserRouter>
        <GlobalStyle />
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/detail/:id" element={<Detail />} />
        </Routes>
      </BrowserRouter>
    </AccountBookProvider>
  );
}

export default App;

 

import BoxContainer from "../components/BoxContainer";
import AccountBookForm from "../components/AccountBookForm";

const Home = () => {
  return (
    <div>
      <h1>Home</h1>
      <AccountBookForm />
      <BoxContainer />
    </div>
  );
};

export default Home;

 

import styled from "styled-components";
import { useState, useEffect, useContext } from "react";
import { v4 as uuidv4 } from "uuid";
import { AccountBookContext } from "../context/AccountBookContext";

const FormWrapper = styled.div`
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
`;

const AccountBookForm = () => {
  const { setMonthData, selectedMonth } = useContext(AccountBookContext);

  const onAdd = (text) => {
    setMonthData((prevData) =>
      prevData.map((month) =>
        month.id === selectedMonth
          ? { ...month, texts: [...month.texts, text] }
          : month
      )
    );
  };

  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({
      ...formData,
      [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";

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

  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";

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 { selectedMonth, setSelectedMonth } = useContext(AccountBookContext);

  const isSelected = selectedMonth === id;

  const handleClick = () => {
    setSelectedMonth(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 { useContext } from "react";
import { AccountBookContext } from "../context/AccountBookContext";

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, selectedMonth } = useContext(AccountBookContext);

  // 옵셔널 체이닝 자세히 알아보자 , 실행 과정을 콘솔로 리턴값 확인해보기
  // 개선해보기
  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, useContext } from "react";
import { useParams, useNavigate, Link } from "react-router-dom";
import { AccountBookContext } from "../context/AccountBookContext";

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, setMonthData, selectedMonth } =
    useContext(AccountBookContext);

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

  const handleSave = () => {
    const updatedText = {
      id,
      date: dateRef.current.value,
      item: itemRef.current.value,
      amount: amountRef.current.value,
      description: descriptionRef.current.value,
    };

    const updatedMonthData = monthData.map((month) => {
      return {
        ...month,
        texts: month.texts.map((text) =>
          text.id === updatedText.id ? updatedText : text
        ),
      };
    });

    setMonthData(updatedMonthData);
    localStorage.setItem("monthData", JSON.stringify(updatedMonthData));
    navigate("/", { state: { selectedMonth } });
  };

  const handleDelete = () => {
    if (window.confirm("정말로 삭제하시겠습니까?")) {
      const deletedMonthData = monthData.map((month) => {
        return {
          ...month,
          texts: month.texts.filter((text) => text.id !== id),
        };
      });

      setMonthData(deletedMonthData);
      localStorage.setItem("monthData", JSON.stringify(deletedMonthData));
      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;

 

import { createGlobalStyle } from "styled-components";
import reset from "styled-reset";

const GlobalStyle = createGlobalStyle`
    ${reset}
    body {
        font-family: "Helvetica", "Arial", sans-serif;
        line-height: 1.5;
        font-size:30px;
    }
`;

export default GlobalStyle;