개발/Project(React-다이어리)

React - 페이지 구현(2) 글 작성 및 수정

잇(IT) 2023. 9. 28. 19:36
728x90

- 위 화면의 왼쪽은 새로운 글을 작성하는 화면, 우측은 작성된 글을 수정하는 화면이다.


- 아래 코드를 통해 자세히 알아보겠다.

 

- New.js

import { useEffect } from "react";
import DiaryEditor from "../components/DiaryEditor";

const New = () => {
  // 상단 title 바꾸기
  useEffect(() => {
    const titleElement = document.getElementsByTagName("title")[0];
    titleElement.innerHTML = `감정 일기장 - 새일기`;
  }, []);

  return (
    <div>
      <DiaryEditor />
    </div>
  );
};

export default New;

- 새로운 일기를 작성하기 위환 화면이다. DiaryEditor 컴포넌트를 사용한다.

 

- Edit.js

import { useContext, useEffect, useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { DiaryStateContext } from "../App";
import DiaryEditor from "../components/DiaryEditor";

const Edit = () => {
  const [originData, setOriginData] = useState();
  const navigate = useNavigate();
  const { id } = useParams();

  // 상단 title 바꾸기
  useEffect(() => {
    const titleElement = document.getElementsByTagName("title")[0];
    titleElement.innerHTML = `감정 일기장 - ${id}번 일기 수정`;
  }, []);

  const diaryList = useContext(DiaryStateContext);

  useEffect(() => {
    if (diaryList.length >= 1) {
      const targetDiary = diaryList.find(
        (it) => parseInt(it.id) === parseInt(id)
      );
      if (targetDiary) {
        setOriginData(targetDiary);
      } else {
        alert("없는 일기입니다.");
        navigate("/", { replace: true });
        // 뒤로 가기 해도 적용이 되지 않도록 하기 위함이다.
      }
    }
  }, [id, diaryList]);

  return (
    <div>
      {originData && <DiaryEditor isEdit={true} originData={originData} />}
    </div>
  );
};

export default Edit;

- 일기는 수정하는 화면에 해당한다. 새 일기를 작성하는 화면과 같이 DiaryEditor 컴포넌트를 사용한다.

 

- DiaryEditor.js

import { useNavigate } from "react-router-dom";
import { useCallback, useContext, useEffect, useRef, useState } from "react";
import { DiaryDispatchContext } from "./../App.js";

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

import { getStringDate } from "../util/date.js";
import { emotionList } from "../util/emotion.js";

const DiaryEditor = ({ isEdit, originData }) => {
  const contentRef = useRef();
  const [content, setContent] = useState("");
  const [emotion, setEmotion] = useState(3);
  const [date, setDate] = useState(getStringDate(new Date()));

  const { onCreate, onEdit, onRemove } = useContext(DiaryDispatchContext);

  const handleClickEmote = useCallback((emotion) => {
    setEmotion(emotion);
  }, []);
  const navigate = useNavigate();

  const handleSubmit = () => {
    if (content.length < 1) {
      contentRef.current.focus();
      return;
    }
    if (
      window.confirm(
        isEdit ? "일기를 수정하시겠습니까?" : "새로운 일기를 작성하시겠습니까?"
      )
    ) {
      if (!isEdit) {
        onCreate(date, content, emotion);
      } else {
        onEdit(originData.id, date, content, emotion);
      }
    }
    navigate("/", { replace: true });
    // 작성 페이지로 못돌아오도록 막는다.
  };

  const handleRemove = () => {
    if (window.confirm("정말 삭제하겠습니까?")) {
      onRemove(originData.id);
      navigate("/", { replace: true });
    }
  };

  useEffect(() => {
    if (isEdit) {
      setDate(getStringDate(new Date(parseInt(originData.date))));
      setEmotion(originData.emotion);
      setContent(originData.content);
    }
  }, [isEdit, originData]);

  return (
    <div className="DiaryEditor">
      <MyHeader
        headText={isEdit ? "일기 수정하기" : "새 일기쓰기"}
        leftChild={
          <MyButton text={"< 뒤로가기"} onClick={() => navigate(-1)} />
        }
        rightChild={
          isEdit && (
            <MyButton
              text={"삭제하기"}
              type={"negative"}
              onClick={handleRemove}
            />
          )
        }
      />
      <div>
        <section>
          <h4>오늘은 언제인가요?</h4>
          <div className="input_box">
            <input
              className="input_date"
              value={date}
              onChange={(e) => setDate(e.target.value)}
              type="date"
            />
          </div>
        </section>
        <section>
          <h4>오늘의 감정</h4>
          <div className="input_box emotion_list_wrapper">
            {emotionList.map((it) => (
              <EmotionItem
                key={it.emotion_id}
                {...it}
                onClick={handleClickEmote}
                isSelected={it.emotion_id === emotion}
              />
            ))}
          </div>
        </section>
        <section>
          <h4>오늘의 일기</h4>
          <div className="input_box text_wrapper">
            <textarea
              placeholder="오늘은 어땠나요?"
              ref={contentRef}
              value={content}
              onChange={(e) => setContent(e.target.value)}
            />
          </div>
        </section>
        <section>
          <div className="control_box">
            <MyButton text={"취소하기"} onClick={() => navigate(-1)} />
            <MyButton
              text={"작성완료"}
              type={"positive"}
              onClick={handleSubmit}
            />
          </div>
        </section>
      </div>
    </div>
  );
};

export default DiaryEditor;

- 새 일기 작성 및 일기 수정 화면에서 사용하는 DiaryEditor 컴포넌트에 해다한다.

 

전체 코드


 

- 위 전체 코드를 아래에서 자세히 알아보겠다.


- New.js

 

const New = () => {
  // 상단 title 바꾸기
  useEffect(() => {
    const titleElement = document.getElementsByTagName("title")[0];
    titleElement.innerHTML = `감정 일기장 - 새일기`;
  }, []);

  return (
    <div>
      <DiaryEditor />
    </div>
  );
};

- 위 코드는 이전에 확인해본 title 태그를 변경하는 코드가 있고, return을 통해 DiaryEditor 컴포넌트를 참조 받는다.


- Edit.js
const Edit = () => {
  const [originData, setOriginData] = useState();
  const navigate = useNavigate();
  const { id } = useParams();

  // 상단 title 바꾸기
  useEffect(() => {
    const titleElement = document.getElementsByTagName("title")[0];
    titleElement.innerHTML = `감정 일기장 - ${id}번 일기 수정`;
  }, []);

- Edit 수정 화면도 마찬가지로 state, 화면 이동을 위한 navigate, 요청 파라미터를 받기 위한 useParams()를 사용했다.

- useParam()의 경우 url 요청으로 넘어오는 파라미터의 값을 받기위해 사용한다.

 

- 또 해당 페이지도 title을 변경하기 위한 코드가 마찬가지로 작성된 것을 볼 수 있다.

 


 

- DiaryEditor.js 

 

1. 필요한 버튼 및 화면 구성하기

 return (
    <div className="DiaryEditor">
      <MyHeader
        headText={isEdit ? "일기 수정하기" : "새 일기쓰기"}
        leftChild={
          <MyButton text={"< 뒤로가기"} onClick={() => navigate(-1)} />
        }
        rightChild={
          isEdit && (
            <MyButton
              text={"삭제하기"}
              type={"negative"}
              onClick={handleRemove}
            />
          )
        }
      />
      <div>
        <section>
          <h4>오늘은 언제인가요?</h4>
          <div className="input_box">
            <input
              className="input_date"
              value={date}
              onChange={(e) => setDate(e.target.value)}
              type="date"
            />
          </div>
        </section>
        <section>
          <h4>오늘의 감정</h4>
          <div className="input_box emotion_list_wrapper">
            {emotionList.map((it) => (
              <EmotionItem
                key={it.emotion_id}
                {...it}
                onClick={handleClickEmote}
                isSelected={it.emotion_id === emotion}
              />
            ))}
          </div>
        </section>
        <section>
          <h4>오늘의 일기</h4>
          <div className="input_box text_wrapper">
            <textarea
              placeholder="오늘은 어땠나요?"
              ref={contentRef}
              value={content}
              onChange={(e) => setContent(e.target.value)}
            />
          </div>
        </section>
        <section>
          <div className="control_box">
            <MyButton text={"취소하기"} onClick={() => navigate(-1)} />
            <MyButton
              text={"작성완료"}
              type={"positive"}
              onClick={handleSubmit}
            />
          </div>
        </section>
      </div>
    </div>
  );
};

1. 가장 위에 뒤로가기 버튼, 현재 일기에 대한 정보, isEdit에 대해 새 일기 작성일 경우 삭제하기 버튼이 없고, 수정하는 일기의 경우 삭제하기 버튼이 생겨나도록 지정한다.

2. navigate(-1)을 사용하게 되면 바로 이전 화면으로 페이지 이동을 하게 된다.

3.isEdit이 true 즉, 수정하는 화면일 경우 삭제하기 버튼이 생겨나고 해당 버튼을 클릭하게 되면 handleRemove 함수가 실행된다.

 

- handleRemove 함수

const handleRemove = () => {
    if (window.confirm("정말 삭제하겠습니까?")) {
      onRemove(originData.id);
      navigate("/", { replace: true });
    }
  };

1. window.confirm을 사용하게 되면 해당 함수가 실행되면 팝업 창으로 확인 및 취소에 대한 팝업 창이 뜨게 된다. 확인하면 함수가 실행이되고, 취소를 누르게 되면 함수가 실행되지 않는다

2. 확인 버튼을 누르게 되어 해당 함수가 실행되게 되면, onRemove 함수가 실행되게 되고, onRemove 함수는 App.js에서 작성한 함수에 해당하고

case "REMOVE": {
      newState = state.filter((it) => it.id !== action.targetId);
      break;
    }

3. onRemove는 함수는 useReducer에 의해 작성되었고, 해당 함수는 filter를 통해 targetId로 넘어온 객체를 제외한 나머지 객체들을 새로운 객체들로 구성한다.

 

- 날짜 선택 버튼

<section>
          <h4>오늘은 언제인가요?</h4>
          <div className="input_box">
            <input
              className="input_date"
              value={date}
              onChange={(e) => setDate(e.target.value)}
              type="date"
            />
          </div>
        </section>

1. type이 date인 섹션에 해당되며, value를 통해 현재 날짜에 대한 정보를 전달한다.

2. onChange 함수를 통해 사용자가 원하는 날짜를 클릭하게 되면 set 함수에 의해 날짜에 대해 변경이 일어난다.

 

- 감정 선택 버튼

<section>
          <h4>오늘의 감정</h4>
          <div className="input_box emotion_list_wrapper">
            {emotionList.map((it) => (
              <EmotionItem
                key={it.emotion_id}
                {...it}
                onClick={handleClickEmote}
                isSelected={it.emotion_id === emotion}
              />
            ))}
          </div>
        </section>

1. emotionList의 경우 별도의 js 파일을 생성하여 감정에 대한 객체 배열을 생성해 놓았다.

export const emotionList = [
  {
    emotion_id: 1,
    emotion_img: process.env.PUBLIC_URL + `/assets/emotion1.png`,
    emotion_descript: "완전 좋음",
  },
  {
    emotion_id: 2,
    emotion_img: process.env.PUBLIC_URL + `/assets/emotion2.png`,
    emotion_descript: "좋음",
  },
  {
    emotion_id: 3,
    emotion_img: process.env.PUBLIC_URL + `/assets/emotion3.png`,
    emotion_descript: "그럭저럭",
  },
  {
    emotion_id: 4,
    emotion_img: process.env.PUBLIC_URL + `/assets/emotion4.png`,
    emotion_descript: "나쁨",
  },
  {
    emotion_id: 5,
    emotion_img: process.env.PUBLIC_URL + `/assets/emotion5.png`,
    emotion_descript: "끔찍함",
  },
];

2. map을 사용할 때 key를 사용하는 이유는 각 컴포넌트를 구분하기 위해 사용하는 것이다.

3. key는 객체 배열에서 각 속성에 만들어 놓은 id값을 사용하고, 각 감정 화면이 구성되고, 각 감정에 대해 클릭을 하게 되면, handleClickEmote 함수가 실행된다. 

const handleClickEmote = useCallback((emotion) => {
    setEmotion(emotion);
  }, []);

4. handleClickEmote 함수가 실행되면 emotion의 값이 클리된 감정으로 선택된다.

5. isSelected Prop에 의해 현재 선택된 emotion과 객체 배열의 emotion.id와 동일한지 검사하고 같을 경우 true를 반환하고, 같지 않을 경우 false를 반환한다.

6. 해당 section은 또 EmotionItem 컴포넌트를 포함하고 있다.

 

- EmotionItem.js
const EmotionItem = ({
  emotion_id,
  emotion_img,
  emotion_descript,
  onClick,
  isSelected,
}) => {
  return (
    <div
      onClick={() => onClick(emotion_id)}
      className={[
        "EmotionItem",
        isSelected ? `EmotionItem_on_${emotion_id}` : `EmotionItem_off`,
      ].join(" ")}
    >
      <img src={emotion_img} />
      <span>{emotion_descript}</span>
    </div>
  );
};

7. EmotionItem props로 받은 값들을 통해 해당 감정이 클릭되면 handleClickEmote 함수의 파라미터로 emotion_id가 넘어가고, setState를 통해 emotion의 값이 변경된다.

8. 또 className을 통해 isSelected 즉, 선택된 section에 대해서 className이 달라지게 구성한다

->  className을 배열로 만들어서 isSelected의 값에 따라 className이 변하도록 지정한다. 또 join을 통해 배열을 다시 문자열로 변환한다.

.EmotionItem_off {
  background-color: #ececec;
}

.EmotionItem_on_1 {
  background-color: #64c964;
  color: white;
}

.EmotionItem_on_2 {
  background-color: #9dd772;
  color: white;

9. 위와 같이 각 className에 대해 색 변화를 넣으면 된다.


- 일기 내용 작성

<section>
          <h4>오늘의 일기</h4>
          <div className="input_box text_wrapper">
            <textarea
              placeholder="오늘은 어땠나요?"
              ref={contentRef}
              value={content}
              onChange={(e) => setContent(e.target.value)}
            />
          </div>
        </section>

1. 위 코드는 일기 내용을 작성하는 코드이다.

2. useRef를 통해 DOM에 직접적으로 접근할 수 있도록 작성하였다.

3.  textarea를 통해 내용을 작성할 수 있는 박스가 나오고, 글이 작성되면 setContent에 의해 변화된 내용이 content에 담기게 된다

4. 또 변하게된 content 내용이 value로 전달된다.

 

- 취소하기 및 작성 완료

<section>
          <div className="control_box">
            <MyButton text={"취소하기"} onClick={() => navigate(-1)} />
            <MyButton
              text={"작성완료"}
              type={"positive"}
              onClick={handleSubmit}
            />
          </div>
        </section>

1. 취소하기 버튼은 간단하게 navigate(-1) 값을 통해 이전 페이지로 돌아가도록 한다.

2. 작성 완료 버튼의 경우 클릭하게 되면 handleSubmit 함수가 동작한다.

const handleSubmit = () => {
    if (content.length < 1) {
      contentRef.current.focus();
      return;
    }
    if (
      window.confirm(
        isEdit ? "일기를 수정하시겠습니까?" : "새로운 일기를 작성하시겠습니까?"
      )
    ) {
      if (!isEdit) {
        onCreate(date, content, emotion);
      } else {
        onEdit(originData.id, date, content, emotion);
      }
    }
    navigate("/", { replace: true });
    // 작성 페이지로 못돌아오도록 막는다.
  };

1. handleSubmit의  첫번째 조건은 일기의 내용이 한글자라도 작성되어야 한다. content.length < 1 즉, 일기가 작성되지 않으면 useRef를 통해 생성한 객체 contentRef에 의해 해당 글 작성 칸으로 focus가 옮겨진다.

2. 두번째는 일기 작성이 통과되고 window.confirm을 통해 팝업을 띄우기 전에 isEdit으로 해당 페이지가 수정 페이지인지 새로운 일기를 작성하는 페이지인지 구분해서 각 상황에 맞는 팝업을 띄운다. 

3. 만약 isEdit이 false일 경우 새로운 일기를 작성하는 상황이기 때문에 onCreate 함수를 호출하여 새로운 일기를 생성한다.

4. 만약 isEdit이 true일 경우 일기를 수정하는 상황이기 때문에 onEdit 함수를 호출하여 일기의 내용을 변경한다.

5. 마지막으로

navigate("/", { replace: true });

코드를 통해 "/" 즉, 홈 화면으로 옮기고 수정되거나 생성된 일기가 뒤로 가기를 한다고 해서 다시 작성하는 페이지로 돌아오지 못하도록 replace: true를 통해 막는다.


- DiaryEditor Props 전달 및 생성
const DiaryEditor = ({ isEdit, originData }) => {
  const contentRef = useRef();
  const [content, setContent] = useState("");
  const [emotion, setEmotion] = useState(3);
  const [date, setDate] = useState(getStringDate(new Date()));

  const { onCreate, onEdit, onRemove } = useContext(DiaryDispatchContext);

- DiaryEditor는 isEdit과 originData props를 전달 받아 사용한다.

- useRef()를 통해 DOM을 참조할 것이고, State 3가지를 사용할 것이다.

- date state의 경우 getStringDate 함수를사용하고 있는데, 

export const getStringDate = (date) => {
  return date.toISOString().slice(0, 10);
};

- 해당 함수는 파라미터로 넘어오는 new Date() 객체를 toISOString() 함수를 통해 특정 형식으로 변한하고 반환한 값을 10글자 즉, 달, 월, 일만 추출하기 위한 함수이다.

 

- 마지막으로 Context를 통해 3개의 컴포넌트를 전달 받는다.

- 각 전달받은 컴포넌트들은 위에서 알아본 바와 같이페이지 구성에 있어서 전부 사용된다.

 

 

728x90