React Emotion Diary - Home


<ホーム>
Home.js
// 1. 현재 date를 구현한다 2. 왼쪽 버튼 3. 오른쪽 버튼

import { useState } from "react";

import MyHeader from "./../components/MyHeader";
import MyButton from "./../components/MyButton";

const Home = () => {
  const [curDate, setCurDate] = useState(new Date());

  // getMonth를 하면 1월 = 0월 이라서 +1을 해야함
  const headText = `${curDate.getFullYear()}년 ${curDate.getMonth() + 1}월`;

  const increaseMonth = () => {
    setCurDate(
      new Date(curDate.getFullYear(), curDate.getMonth() + 1, curDate.getDate())
    );
  };

  const decreaseMonth = () => {
    setCurDate(
      new Date(curDate.getFullYear(), curDate.getMonth() - 1, curDate.getDate())
    );
  };

  return (
    <div>
      <MyHeader
        headText={headText}
        leftChild={<MyButton text={"<"} onClick={decreaseMonth} />}
        rightChild={<MyButton text={">"} onClick={increaseMonth} />}
      />
    </div>
  );
};

export default Home;
実行画面

<コンテンツの読み込み>
DiaryList.js
const DiaryList = ({ diaryList }) => {
  return (
    <div>
      {/* props로 전달받은 diaryList를 map으로 리스트 렌더링 */}
      {diaryList.map((it) => (
        <div key={it.id}>{it.content}</div>
      ))}
    </div>
  );
};

// diaryList prop이 정상적으로 전달이 안될 수 있으므로 default props 사용
DiaryList.defaultProps = {
    diaryList: [],
}

export default DiaryList;
Home.js
import { useContext, useEffect, useState } from "react";
import { DiaryStateContext } from "../App";

import MyHeader from "./../components/MyHeader";
import MyButton from "./../components/MyButton";
import DiaryList from "../components/DiaryList";

const Home = () => {
  const diaryList = useContext(DiaryStateContext);

  const [data, setData] = useState([]);
  const [curDate, setCurDate] = useState(new Date());

  const headText = `${curDate.getFullYear()}년 ${curDate.getMonth() + 1}월`;

  //curDate가 변화하는 순간에만 useEffecr로 년도와 월에 해당하는 일기 데이터만 뽑아옴
  useEffect(() => {
    // diaryList가 비어있는 상황에서는 동작될 필요가 없으므로 1 이상일 경우에만 동작해라
    if (diaryList.length >= 1) {
    }
    const firstDay = new Date(
      curDate.getFullYear(),
      curDate.getMonth(),
      // 이번년도 이번월의 1일이 됨
      1
    ).getTime();

    const lastDay = new Date(
      curDate.getFullYear(),
      curDate.getMonth() + 1,
      0
    ).getTime();

    setData(
      diaryList.filter((it) => firstDay <= it.date && it.date <= lastDay)
    );
    // diaryList가 바꼈다는건 일기가 새로 추가 됐거나, 수정 됐거나, 삭제 됐다는걸 의미 => List도 변경 해야 함
  }, [diaryList, curDate]);

  useEffect(() => {
    console.log(data);
  }, [data]);

  const increaseMonth = () => {
    setCurDate(
      new Date(curDate.getFullYear(), curDate.getMonth() + 1, curDate.getDate())
    );
  };

  const decreaseMonth = () => {
    setCurDate(
      new Date(curDate.getFullYear(), curDate.getMonth() - 1, curDate.getDate())
    );
  };

  return (
    <div>
      <MyHeader
        headText={headText}
        leftChild={<MyButton text={"<"} onClick={decreaseMonth} />}
        rightChild={<MyButton text={">"} onClick={increaseMonth} />}
      />
      <DiaryList diaryList={data} />
    </div>
  );
};

export default Home;
App.js
import React, { useReducer, useRef } from "react";

import "./App.css";
import { BrowserRouter, Route, Routes } from "react-router-dom";

import Home from "./pages/Home";
import New from "./pages/New";
import Edit from "./pages/Edit";
import Diary from "./pages/Diary";

const reducer = (state, action) => {
  let newState = [];
  switch (action.type) {
    case "INIT": {
      return action.data;
    }
    case "CREATE": {
      newState = [action.data, ...state];
      break;
    }
    case "REMOVE": {
      newState = state.filter((it) => it.id !== action.targetId);
      break;
    }
    case "EDIT": {
      newState = state.map((it) =>
        it.id === action.data.id
          ? {
              ...action.data,
            }
          : it
      );
      break;
    }
    default:
      return state;
  }

  return newState;
};

export const DiaryStateContext = React.createContext();
export const DiaryDispatchContext = React.createContext();

const dummyData = [
  {
    id: 1,
    emotion: 1,
    content: "오늘의일기 1번",
    // date의 값은 ms로 넣어야 함
    date: 1648633885549,
  },
  {
    id: 2,
    emotion: 2,
    content: "오늘의일기 2번",
    // date의 값은 ms로 넣어야 함
    date: 1648633885550,
  },
  {
    id: 3,
    emotion: 3,
    content: "오늘의일기 2번",
    // date의 값은 ms로 넣어야 함
    date: 1648633885551,
  },
  {
    id: 4,
    emotion: 4,
    content: "오늘의일기 4번",
    // date의 값은 ms로 넣어야 함
    date: 1648633885552,
  },
  {
    // 가장 최신의 일기글
    id: 5,
    emotion: 5,
    content: "오늘의일기 5번",
    // date의 값은 ms로 넣어야 함
    date: 1648633885553,
  },
];

function App() {
  // 확인을 위해서 []에 dummyData를 기초 값으로 넣어줌
  const [data, dispatch] = useReducer(reducer, dummyData);

  // 현재 date의 ms 구하는 방법
  console.log(new Date().getTime());

  const dataId = useRef(0);

  const onCreate = (date, content, emotion) => {
    dispatch({
      type: "CREATE",
      data: {
        id: dataId.current,
        date: new Date(date).getTime(),
        content,
        emotion,
      },
    });
    dataId.current += 1;
  };

  const onRemove = (targetId) => {
    dispatch({ type: "REMOVE", targetId });
  };

  const onEdit = (targetId, date, content, emotion) => {
    dispatch({
      type: "EDIT",

      data: {
        id: targetId,
        date: new Date(date).getTime(),
        content,
        emotion,
      },
    });
  };

  return (
    <DiaryStateContext.Provider value={data}>
      <DiaryDispatchContext.Provider value={(onCreate, onRemove, onEdit)}>
        <BrowserRouter>
          <div className="App">
            <Routes>
              <Route path="/" element={<Home />} />
              <Route path="/new" element={<New />} />
              <Route path="/edit" element={<Edit />} />
              <Route path="/diary/:id" element={<Diary />} />
            </Routes>
          </div>
        </BrowserRouter>
      </DiaryDispatchContext.Provider>
    </DiaryStateContext.Provider>
  );
}

export default App;
実行画面

<旧順、最新順>
DiaryList.js
import { useState } from "react";

const sortOptionList = [
  { value: "latest", name: "latest" },
  { value: "oldest", name: "oldest" },
];

// value = control 메뉴가 렌더링 하는 select가 어떤걸 선택 하고 있는지 역할
// onChange = select가 선택하는게 변화 했을때 바꿀 기능을 할 함수
// optionList = select 태그안에 들어갈 옵션
const ControlMenu = ({ value, onChange, optionList }) => {
  return (
    //onChange 이벤트가 일어나게 되면 이벤트 이벤트 객체에 target에 value를 전달해서  prop으로 받은 onChange 매서드를 실행 시키는데
    // prop으로 준 onChange 메서드는 setSortType이였기 때문에 sortType을 oldest를 선택하면 oldest가 되고, latest를 선택하면 latest가 됨
    <select value={value} onChange={(e) => onChange(e.target.value)}>
      {/* it = sortOptionList에 latest,oldest 객체를 가르킴 */}
      {optionList.map((it, idx) => (
        <option key={idx} value={it.value}>
          {it.name}
        </option>
      ))}
    </select>
  );
};

const DiaryList = ({ diaryList }) => {
  // 정렬 기준을 저장할 state
  //sortType 정렬기준을 바꾸는 select의 역할
  const [sortType, setSortType] = useState("latest");

  // 최신순인지 오래된순인지 if문으로 분기를 달아서 정렬된 리스트를 반환하는 역할을 하는 함수
  const getProcessDiaryList = () => {
    // 정렬하고 하는 데이터가 배열이면 그냥 정렬이 안됨 => 비교 함수를 만들어 줘야함
    const compare = (a, b) => {
      if (sortType === "latest") {
        // 문자열이 들어올 수 있기 때문에 parseInt
        //date값을 비교해 가장 최신에 작성한게 앞
        return parseInt(b.date) - parseInt(a.date);
      } else {
        return parseInt(a.date) - parseInt(b.date);
      }
    };

    //sort 하면 원본 배열이 정렬 되므로 copy해 사용
    // JSON.stringify(diaryList) => diaryList가 배열이어서 배열을 JSON화 시켜서 문자열로 바꿈
    // 문자열로 반환된걸 JSON.parse를 수행 시키게 되면 다시 배열로 복호화
    // diaryList에 있는 원본 배열의 값이 문자열로 바꼈다가 다시 배열로 바껴서 값만 들어옴 => DiaryList가 저장하고 있는 배열을 건드리지 않을 수 있음
    const copyList = JSON.parse(JSON.stringify(diaryList));
    const sortedList = copyList.sort(compare);
    return sortedList;
  };

  return (
    <div>
      <ControlMenu
        value={sortType}
        onChange={setSortType}
        optionList={sortOptionList}
      />
      {getProcessDiaryList().map((it) => (
        <div key={it.id}>{it.content}</div>
      ))}
    </div>
  );
};

DiaryList.defaultProps = {
  diaryList: [],
};

export default DiaryList;
実行画面

<感情フィルター>
DiaryList.js
import { useState } from "react";

const sortOptionList = [
  { value: "latest", name: "latest" },
  { value: "oldest", name: "oldest" },
];

const filterOptionList = [
  { value: "all", name: "all" },
  { value: "good", name: ":)" },
  { vlaue: "bad", name: ":(" },
];

// value = 현재 filter의 값, onChange = setFilter 필터를 바꾸는 함수, optionList = filterOptionList
const ControlMenu = ({ value, onChange, optionList }) => {
  return (
    <select value={value} onChange={(e) => onChange(e.target.value)}>
      {optionList.map((it, idx) => (
        <option key={idx} value={it.value}>
          {it.name}
        </option>
      ))}
    </select>
  );
};

const DiaryList = ({ diaryList }) => {
  const [sortType, setSortType] = useState("latest");
  //모든 감정을 표현하기 위해서 기본 값 = all
  const [filter, setFilter] = useState("all");

  const getProcessDiaryList = () => {
    const filterCallBack = (item) => {
      if (filter === "good") {
        // 산술 연산자 사용할거면 emotion도 숫자여야 하는데 항상 숫자일거라는 보장이 없기 때문에 parseInt로 숫자로 형변환
        return parseInt(item.emotion) <= 3;
      } else {
        return parseInt(item.emotion) > 3;
      }
    };

    const compare = (a, b) => {
      if (sortType === "latest") {
        return parseInt(b.date) - parseInt(a.date);
      } else {
        return parseInt(a.date) - parseInt(b.date);
      }
    };

    const copyList = JSON.parse(JSON.stringify(diaryList));

    // filterCallBack에 it을 전달했을때 return ture를 반환하는 애들로만 필터링을 해라
    const filteredList =
      filter === "all" ? copyList : copyList.filter((it) => filterCallBack(it));

    const sortedList = filteredList.sort(compare);
    return sortedList;
  };

  return (
    <div>
      <ControlMenu
        value={sortType}
        onChange={setSortType}
        optionList={sortOptionList}
      />
      <ControlMenu
        value={filter}
        onChange={setFilter}
        optionList={filterOptionList}
      />
      {getProcessDiaryList().map((it) => (
        <div key={it.id}>
          {it.content} {it.emotion}
        </div>
      ))}
    </div>
  );
};

DiaryList.defaultProps = {
  diaryList: [],
};

export default DiaryList;
実行画面

『新しい日記を書く』
DiaryList.js
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import MyButton from "./MyButton";

const sortOptionList = [
  { value: "latest", name: "latest" },
  { value: "oldest", name: "oldest" },
];

const filterOptionList = [
  { value: "all", name: "all" },
  { value: "good", name: ":)" },
  { vlaue: "bad", name: ":(" },
];

const ControlMenu = ({ value, onChange, optionList }) => {
  return (
    <select value={value} onChange={(e) => onChange(e.target.value)}>
      {optionList.map((it, idx) => (
        <option key={idx} value={it.value}>
          {it.name}
        </option>
      ))}
    </select>
  );
};

const DiaryList = ({ diaryList }) => {
  const navigate = useNavigate();
  const [sortType, setSortType] = useState("latest");

  const [filter, setFilter] = useState("all");

  const getProcessDiaryList = () => {
    const filterCallBack = (item) => {
      if (filter === "good") {
        return parseInt(item.emotion) <= 3;
      } else {
        return parseInt(item.emotion) > 3;
      }
    };

    const compare = (a, b) => {
      if (sortType === "latest") {
        return parseInt(b.date) - parseInt(a.date);
      } else {
        return parseInt(a.date) - parseInt(b.date);
      }
    };

    const copyList = JSON.parse(JSON.stringify(diaryList));

    const filteredList =
      filter === "all" ? copyList : copyList.filter((it) => filterCallBack(it));

    const sortedList = filteredList.sort(compare);
    return sortedList;
  };

  return (
    <div>
      <ControlMenu
        value={sortType}
        onChange={setSortType}
        optionList={sortOptionList}
      />
      <ControlMenu
        value={filter}
        onChange={setFilter}
        optionList={filterOptionList}
      />
      {/* onClick을 누르면 navigate 함수를 호출해서 /new 경로를 가지는 페이지로 페이지 이동이 가능함 */}
      <MyButton
        type={"positive"}
        text={"write a diary"}
        onClick={() => navigate("/new")}
      />
      {getProcessDiaryList().map((it) => (
        <div key={it.id}>
          {it.content} {it.emotion}
        </div>
      ))}
    </div>
  );
};

DiaryList.defaultProps = {
  diaryList: [],
};

export default DiaryList;
実行画面

<ホームページcss>
DiaryList.js
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import MyButton from "./MyButton";

const sortOptionList = [
  { value: "latest", name: "latest" },
  { value: "oldest", name: "oldest" },
];

const filterOptionList = [
  { value: "all", name: "all" },
  { value: "good", name: ":)" },
  { vlaue: "bad", name: ":(" },
];

const ControlMenu = ({ value, onChange, optionList }) => {
  return (
    <select
      className="ControlMenu"
      value={value}
      onChange={(e) => onChange(e.target.value)}
    >
      {optionList.map((it, idx) => (
        <option key={idx} value={it.value}>
          {it.name}
        </option>
      ))}
    </select>
  );
};

const DiaryList = ({ diaryList }) => {
  const navigate = useNavigate();
  const [sortType, setSortType] = useState("latest");

  const [filter, setFilter] = useState("all");

  const getProcessDiaryList = () => {
    const filterCallBack = (item) => {
      if (filter === "good") {
        return parseInt(item.emotion) <= 3;
      } else {
        return parseInt(item.emotion) > 3;
      }
    };

    const compare = (a, b) => {
      if (sortType === "latest") {
        return parseInt(b.date) - parseInt(a.date);
      } else {
        return parseInt(a.date) - parseInt(b.date);
      }
    };

    const copyList = JSON.parse(JSON.stringify(diaryList));

    const filteredList =
      filter === "all" ? copyList : copyList.filter((it) => filterCallBack(it));

    const sortedList = filteredList.sort(compare);
    return sortedList;
  };

  return (
    <div className="DiaryList">
      <div className="menu_wrapper">
        <div className="left_col">
          <ControlMenu
            value={sortType}
            onChange={setSortType}
            optionList={sortOptionList}
          />
          <ControlMenu
            value={filter}
            onChange={setFilter}
            optionList={filterOptionList}
          />
        </div>
        <div className="right_col">
          <MyButton
            type={"positive"}
            text={"write a diary"}
            onClick={() => navigate("/new")}
          />
        </div>
      </div>

      {getProcessDiaryList().map((it) => (
        <div key={it.id}>
          {it.content} {it.emotion}
        </div>
      ))}
    </div>
  );
};

DiaryList.defaultProps = {
  diaryList: [],
};

export default DiaryList;
実行画面

<画像、文字、ボタン>
DiaryItem.js
import { Navigate, useNavigate } from "react-router-dom";
import MyButton from "./MyButton";

const DiaryItem = ({ id, emotion, content, date }) => {
  const navigate = useNavigate();

  // 이미지가 안뜨면
  const env = process.env;
  env.PUBLIC_URL = env.PUBLIC_URL || "";

  const strDate = new Date(parseInt(date)).toLocaleDateString();

  // 일기를 조회할 함수
  const goDetail = () => {
    navigate(`/diary/${id}`);
  };

  // 일기를 수정할 함수
  const goEdit = () => {
    navigate(`/edit/${id}`);
  };

  return (
    <div className={"DiaryItem"}>
      <div
        onClick={goDetail}
        className={[
          "emotion_img_wrapper",
          `emotion_img-wrapper_${emotion}`,
          // 감정에 따라서 동적으로 className을 정해줄 수 있음
        ].join(" ")}
      >
        {/* process.env.PUBLIC_URL = public 디렉토리 주소 */}
        {/* ${emotion} =>  감정의 숫자가 들어옴 */}
        <img src={process.env.PUBLIC_URL + `/assets/emotion${emotion}.png`} />
      </div>
      <div onClick={goDetail} className="info_wrapper">
        <div className="diary_date">{strDate}</div>
        <div className="diary_content_preview">{content.slice(0, 25)}</div>
      </div>
      <div className="btn_wrapper">
        <MyButton onClick={goEdit} text={"edit"} />
      </div>
    </div>
  );
};

export default DiaryItem;
DiaryList.js
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import DiaryItem from "./DiaryItem";
import MyButton from "./MyButton";

const sortOptionList = [
  { value: "latest", name: "latest" },
  { value: "oldest", name: "oldest" },
];

const filterOptionList = [
  { value: "all", name: "all" },
  { value: "good", name: ":)" },
  { vlaue: "bad", name: ":(" },
];

const ControlMenu = ({ value, onChange, optionList }) => {
  return (
    <select
      className="ControlMenu"
      value={value}
      onChange={(e) => onChange(e.target.value)}
    >
      {optionList.map((it, idx) => (
        <option key={idx} value={it.value}>
          {it.name}
        </option>
      ))}
    </select>
  );
};

const DiaryList = ({ diaryList }) => {
  const navigate = useNavigate();
  const [sortType, setSortType] = useState("latest");

  const [filter, setFilter] = useState("all");

  const getProcessedDiaryList = () => {
    const filterCallBack = (item) => {
      if (filter === "good") {
        return parseInt(item.emotion) <= 3;
      } else {
        return parseInt(item.emotion) > 3;
      }
    };

    const compare = (a, b) => {
      if (sortType === "latest") {
        return parseInt(b.date) - parseInt(a.date);
      } else {
        return parseInt(a.date) - parseInt(b.date);
      }
    };

    const copyList = JSON.parse(JSON.stringify(diaryList));

    const filteredList =
      filter === "all" ? copyList : copyList.filter((it) => filterCallBack(it));

    const sortedList = filteredList.sort(compare);
    return sortedList;
  };

  return (
    <div className="DiaryList">
      <div className="menu_wrapper">
        <div className="left_col">
          <ControlMenu
            value={sortType}
            onChange={setSortType}
            optionList={sortOptionList}
          />
          <ControlMenu
            value={filter}
            onChange={setFilter}
            optionList={filterOptionList}
          />
        </div>
        <div className="right_col">
          <MyButton
            type={"positive"}
            text={"write a diary"}
            onClick={() => navigate("/new")}
          />
        </div>
      </div>

      {getProcessedDiaryList().map((it) => (
        // DiaryItem 컴포넌트 사용할 곳
        <DiaryItem key={it.id} {...it} />
      ))}
    </div>
  );
};

DiaryList.defaultProps = {
  diaryList: [],
};

export default DiaryList;
実行画面

<すべてcss>
App.css
@import url("https://fonts.googleapis.com/css2?family=Nanum+Pen+Script&family=Yeon+Sung&display=swap");

body {
  background-color: #f0ffff;
  display: flex;
  /*body태그 아래에 있는 요소들을 body 태그를 기준으로 가운데 위치 */
  justify-content: center;
  /*display가 flex 속성으로 있을때 세로축을 기준으로 가운데 두겠다 */
  align-items: center;
  /*실제 웹 스크린의 100%를 최소 높이로 갖겠다로 선언 */
  font-family: "Nanum Pen Script";
  margin: 0px;

  min-height: 100vh;
}
/*min-width => 괄호 안에 있는 모든 css 규칙들을 웹 브라우저의 화면이 650px 이상일때만 적용*/
/* 반응형 웹을 도와주는 css 도구 */
@media (min-width: 650px) {
  .App {
    width: 640px;
  }
}
/* 웹 브라우저의 화면이 650px 이하일 경우에 App이라는 컴포넌트는 90%를 차지하게 하겠음 */
@media (max-width: 650px) {
  .App {
    width: 90vw;
  }
}

#root {
  background-color: white;
  box-shadow: rgba(100, 100, 111, 0.2) 0px 7px 29px 0px;
}

/* 기본적으로 100%를 차지하는 높이를 갖게 됨 */
.App {
  min-height: 100vh;
  padding-left: 20px;
  padding-right: 20px;
}

/*  MyButton */

.MyButton {
  cursor: pointer;
  border: none;
  border-radius: 5px;

  padding-top: 10px;
  padding-bottom: 10px;
  padding-left: 20px;
  padding-right: 20px;

  font-size: 18px;

  /* 글자가 잘려서 두줄이 되지 않게 하는 속성 */
  white-space: nowrap;
  font-family: "Nanum Pen Script";
}

.MyButton_default {
  background-color: #ececec;
  color: black;
}

.MyButton_positive {
  background-color: #ffccff;
}

.MyButton_negative {
  background-color: #ffe4c0;
}

/* Header */

header {
  padding-top: 20px;
  padding-bottom: 20px;

  /* flex 속성을 주게 되면 <div> 가로로 바뀐다 */
  display: flex;
  align-items: center;
  border-bottom: 1px solid #e2e2e2;
}

header > div {
  display: flex;
}

/* justify-center:center head 텍스트 중앙에 위치 시킨다 */
header .head_text {
  width: 50%;

  font-size: 25px;
  justify-content: center;
}

header .head_btn_left {
  width: 25%;
  justify-content: start;
}

header .head_btn_right {
  width: 25%;
  justify-content: end;
}

header button {
  font-family: "Nanum Pen Script", cursive;
}

/*DiaryList*/

.DiaryList .menu_wrapper {
  margin-top: 20px;
  margin-bottom: 30px;

  /* div 요소들이 한줄로 정렬 */
  display: flex;

  /* 왼쪽 div,  오른쪽 div 간격이 완전히 벌어지게 됨 */
  justify-content: space-between;
}

.DiaryList .menu_wrapper .right_col {
  /* display flex 아래 모든 넓이를 자신이 차지하게 됨 => right-col 남은 영역 전체의 넓이를 갖게 됨 */
  flex-grow: 1;
}

.DiaryList .menu_wrapper .right_col button {
  width: 100%;
}

.DiaryList .ControlMenu {
  margin-right: 10px;
  border: none;
  border-radius: 5px;
  background-color: #ececec;

  padding-top: 10px;
  padding-bottom: 10px;
  padding-left: 20px;
  padding-right: 20px;

  cursor: pointer;
  font-family: "Nanum pen Script";
  font-size: 18px;
}

/* DiaryItem */

.DiaryItem {
  padding-top: 15px;
  padding-bottom: 15px;

  border-bottom: 1px solid #e2e2e2;

  display: flex;
  justify-content: space-between;
}

.DiaryItem .emotion_img_wrapper {
  cursor: pointer;
  min-width: 120px;
  height: 80px;
  border-radius: 5px;
  display: flex;
  justify-content: center;
}
.DiaryItem .info_wrapper {
  flex-grow: 1;
  margin-left: 20px;
  cursor: pointer;
}

.DiaryItem .diary_date {
  font-weight: bold;
  font-size: 25px;
  margin-bottom: 5px;
}

.DiaryItem .diary_content_preview {
  font-size: 18px;
}

.DiaryItem .btn_wrapper {
  min-width: 70px;
}