관리 메뉴

거니의 velog

(15) 리액트와 API 서버 통신 5 본문

SpringBoot_React 풀스택 프로젝트

(15) 리액트와 API 서버 통신 5

Unlimited00 2024. 2. 29. 11:42

7. 수정/삭제 처리

* 수정/삭제 화면은 기존의 ModifyPage와 새롭게 작성할 ModifyComponent를 이용해서 처리한다. 코드를 구현하기 전에 수정/삭제 후에는 어떤 식으로 동작해야 하는지를 정리해 본다.

- 삭제(Delete 버튼) : 삭제 결과를 모달창으로 보여주고 '/todo/list'로 이동

- 수정(Modify 버튼) : 수정 결과를 모달창으로 보여주고 '/todo/read/번호'로 이동

* 수정/삭제 기능 모두 서버의 호출 결과를 이용해서 모달창을 보여주는 점은 공통적이고 이후의 이동 경로에서 차이가 난다.


(1) 수정/삭제 호출 기능 작성

* api/todoApi.js에는 수정과 삭제에 필요한 함수들을 추가한다.

import axios from "axios";

export const API_SERVER_HOST = "http://localhost:8080";

const prefix = `${API_SERVER_HOST}/api/todo`;

export const getOne = async (tno) => {
  const res = await axios.get(`${prefix}/${tno}`);
  return res.data;
};

export const getList = async (pageParam) => {
  const { page, size } = pageParam;
  const res = await axios.get(`${prefix}/list`, {
    params: { page: page, size: size },
  });
  return res.data;
};

export const postAdd = async (todoObj) => {
  const res = await axios.post(`${prefix}/`, todoObj);
  return res.data;
};

export const deleteOne = async (tno) => {
  const res = await axios.delete(`${prefix}/${tno}`);
  return res.data;
};

export const putOne = async (todo) => {
  const res = await axios.put(`${prefix}/${todo.tno}`, todo);
  return res.data;
};

(2) 수정/삭제를 위한 컴포넌트

* 수정/삭제의 실제 처리는 components/todo/ModifyComponent를 추가해서 처리한다. 라우팅을 설계하는 단계에서는 수정/삭제 버튼을 페이지 역할을 하는 ModifyPage에서 작성했지만, 더 일관성 있는 개발을 위해서 버튼들을 ModifyComponent에서 처리하도록 변경할 것이다.

* components/todo 폴더에 ModifyComponent를 추가하고 버튼들과 필요한 상태(state) 데이터들을 준비한다.

import React, { useEffect, useState } from "react";

const initState = {
  tno: 0,
  title: "",
  writer: "",
  dueDate: "",
  complete: false,
};

const ModifyComponent = ({ tno }) => {
  const [todo, setTodo] = useState({ ...initState });

  useEffect(() => {}, [tno]);

  return (
    <div className="border-2 border-sky-200 mt-10 m-2 p-4">
      <div className="flex justify-end p-4">
        <button
          type="button"
          className="rounded p-4 m-2 text-xl w-32 text-white bg-red-500"
        >
          Delete
        </button>
        <button
          type="button"
          className="rounded p-4 m-2 text-xl w-32 text-white bg-blue-500"
        >
          Modify
        </button>
      </div>
    </div>
  );
};

export default ModifyComponent;

* ModifyPage 에서는 ModifyComponent를 import 해서 필요한 속성들을 지정한다.

import React from "react";
import { useParams } from "react-router-dom";
import ModifyComponent from "../../components/todo/ModifyComponent";

const ModifyPage = () => {
  const { tno } = useParams();

  return (
    <div className="p-4 w-full bg-white">
      <div className="text-3xl font-extrabold">Todo Modify Page</div>
      <ModifyComponent tno={tno} />
    </div>
  );
};

export default ModifyPage;

* 실제 수정을 위한 내용은 구현되지 않은 상황이므로 화면에서는 ModifyComponent가 가진 테두리와 버튼들만이 보인다. 브라우저에서 특정 Todo를 조회한 후에 'Modify' 버튼을 클릭해서 수정/삭제 화면으로 이동하는 것을 확인한다.


(3) 서버 데이터 출력

* ModifyComponent는 우선 서버에서 내용물을 가져와서 출력하고 변경 가능하도록 구성할 필요가 있다. 이를 위해서 조회 기능과 같이 useEffect()를 사용해서 서버로부터 데이터를 가져오고, 등록 기능에 사용했던 <input>을 이용한다.

import React, { useEffect, useState } from "react";
import { getOne } from "../../api/todoApi";

const initState = {
  tno: 0,
  title: "",
  writer: "",
  dueDate: "",
  complete: false,
};

const ModifyComponent = ({ tno }) => {
  const [todo, setTodo] = useState({ ...initState });

  useEffect(() => {
    getOne(tno).then((data) => setTodo(data));
  }, [tno]);

  const handleChangeTodo = (e) => {
    todo[e.target.name] = e.target.value;
    setTodo({ ...todo });
  };

  const handleChangeTodoComplete = (e) => {
    const value = e.target.value;
    todo.complete = value === "Y";
    setTodo({ ...todo });
  };

  return (
    <div className="border-2 border-sky-200 mt-10 m-2 p-4">
      <div className="flex justify-center mt-10">
        <div className="relative mb-4 flex w-full flex-wrap items-stretch">
          <div className="w-1/5 p-6 text-right font-bold">TNO</div>
          <div className="w-4/5 p-6 rounded-r border border-solid shadow-md bg-gray-100">
            {todo.tno}
          </div>
        </div>
      </div>

      <div className="flex justify-center">
        <div className="relative mb-4 flex w-full flex-wrap items-stretch">
          <div className="w-1/5 p-6 text-right font-bold">WRITER</div>
          <div className="w-4/5 p-6 rounded-r border border-solid shadow-md bg-gray-100">
            {todo.writer}
          </div>
        </div>
      </div>

      <div className="flex justify-center">
        <div className="relative mb-4 flex w-full flex-wrap items-stretch">
          <div className="w-1/5 p-6 text-right font-bold">TITLE</div>
          <input
            className="w-4/5 p-6 rounded-r border border-solid border-neutral-300 shadow-md"
            name="title"
            type={"text"}
            value={todo.title}
            onChange={handleChangeTodo}
          ></input>
        </div>
      </div>

      <div className="flex justify-center">
        <div className="relative mb-4 flex w-full flex-wrap items-stretch">
          <div className="w-1/5 p-6 text-right font-bold">DUEDATE</div>
          <input
            className="w-4/5 p-6 rounded-r border border-solid border-neutral-300 shadow-md"
            name="dueDate"
            type={"date"}
            value={todo.dueDate}
            onChange={handleChangeTodo}
          ></input>
        </div>
      </div>

      <div className="flex justify-center">
        <div className="relative mb-4 flex w-full flex-wrap items-stretch">
          <div className="w-1/5 p-6 text-right font-bold">COMPLETE</div>
          <select
            name="status"
            className="border-solid border-2 rounded m-1 p-2"
            onChange={handleChangeTodoComplete}
            value={todo.complete ? "Y" : "N"}
          >
            <option value="Y">Completed</option>
            <option value="N">Not Yet</option>
          </select>
        </div>
      </div>

      <div className="flex justify-end p-4">
        <button
          type="button"
          className="rounded p-4 m-2 text-xl w-32 text-white bg-red-500"
        >
          Delete
        </button>
        <button
          type="button"
          className="rounded p-4 m-2 text-xl w-32 text-white bg-blue-500"
        >
          Modify
        </button>
      </div>
    </div>
  );
};

export default ModifyComponent;

* 화면에서 수정이 가능한 부분은 <input>을 이용해서 처리하고 수정이 불가능한 부분은 <div>를 이용해서 출력한다.


(4) 수정/삭제와 모달창

* ModifyComponent의 기능들은 호출 후에 결과를 보여주기 때문에 모달창을 추가해야 한다. 삭제 후 모달창을 보여주고 난 후에 모달창이 닫히면서 삭제하도록 구성해야 하므로 화면에 ResultModal을 이용해서 이를 처리한다.

* 화면 이동 전에 수정이나 삭제 호출에 문제가 없는지 확인하기 위해서 handleClickModify()나 handleClickDelete()와 같은 함수를 지정하고 버튼에 이벤트 처리를 추가한다.

import React, { useEffect, useState } from "react";
import { getOne, putOne, deleteOne } from "../../api/todoApi";

const initState = {
  tno: 0,
  title: "",
  writer: "",
  dueDate: "",
  complete: false,
};

const ModifyComponent = ({ tno }) => {
  const [todo, setTodo] = useState({ ...initState });
  // 모달창을 위한 상태
  const [result, setResult] = useState(null);

  useEffect(() => {
    getOne(tno).then((data) => setTodo(data));
  }, [tno]);

  const handleClickModify = () => {
    //버튼 클릭시
    putOne(todo).then((data) => {
      console.log("modify result: " + data);
      setResult("Modified");
    });
  };

  const handleClickDelete = () => {
    //버튼 클릭시
    deleteOne(tno).then((data) => {
      console.log("delete result: " + data);
      setResult("Deleted");
    });
  };

  (...)

  return (
    <div className="border-2 border-sky-200 mt-10 m-2 p-4">
      (...)

      <div className="flex justify-end p-4">
        <button
          type="button"
          className="inline-block rounded p-4 m-2 text-xl w-32  text-white bg-red-500"
          onClick={handleClickDelete}
        >
          Delete
        </button>
        <button
          type="button"
          className="rounded p-4 m-2 text-xl w-32 text-white bg-blue-500"
          onClick={handleClickModify}
        >
          Modify
        </button>
      </div>
    </div>
  );
};

export default ModifyComponent;

* 브라우저상에서 수정/삭제 기능이 정상적으로 동작하는지 확인한다. 아직 화면 이동이 없기 때문에 개발자 도구의 Console 탭과 Network 탭을 이용해서 결과를 확인한다.


1. 삭제 테스트


2. 수정 테스트


* 정상적으로 서버의 호출이 이루어지는 것을 확인했다면 useCustomMove를 이용해서 화면 이동에 필요한 기능들을 가져오고 ResultModal이 close 될 때 호출하도록 변경한다.

import React, { useEffect, useState } from "react";
import { getOne, putOne, deleteOne } from "../../api/todoApi";
import useCustomMove from "../../hooks/useCustomMove";
import ResultModal from "../common/ResultModal";

const initState = {
  tno: 0,
  title: "",
  writer: "",
  dueDate: "",
  complete: false,
};

const ModifyComponent = ({ tno }) => {
  const [todo, setTodo] = useState({ ...initState });
  //모달 창을 위한 상태
  const [result, setResult] = useState(null);
  //이동을 위한 기능들
  const { moveToList, moveToRead } = useCustomMove();

  const handleClickModify = () => {
    putOne(todo).then((data) => {
      console.log("modify result: " + data);
      setResult("Modified");
    });
  };

  const handleClickDelete = () => {
    deleteOne(tno).then((data) => {
      console.log("delete result: " + data);
      setResult("Deleted");
    });
  };

  //모달 창이 close될 때
  const closeModal = () => {
    if (result === "Deleted") {
      moveToList();
    } else {
      moveToRead(tno);
    }
  };

  useEffect(() => {
    getOne(tno).then((data) => setTodo(data));
  }, [tno]);

  const handleChangeTodo = (e) => {
    todo[e.target.name] = e.target.value;
    setTodo({ ...todo });
  };

  const handleChangeTodoComplete = (e) => {
    const value = e.target.value;
    todo.complete = value === "Y";
    setTodo({ ...todo });
  };

  return (
    <div className="border-2 border-sky-200 mt-10 m-2 p-4">
      {result ? (
        <ResultModal
          title={"처리결과"}
          content={result}
          callbackFn={closeModal}
        ></ResultModal>
      ) : (
        <></>
      )}

      <div className="flex justify-center mt-10">
        <div className="relative mb-4 flex w-full flex-wrap items-stretch">
          <div className="w-1/5 p-6 text-right font-bold">TNO</div>
          <div className="w-4/5 p-6 rounded-r border border-solid shadow-md bg-gray-100">
            {todo.tno}
          </div>
        </div>
      </div>

      <div className="flex justify-center">
        <div className="relative mb-4 flex w-full flex-wrap items-stretch">
          <div className="w-1/5 p-6 text-right font-bold">WRITER</div>
          <div className="w-4/5 p-6 rounded-r border border-solid shadow-md bg-gray-100">
            {todo.writer}
          </div>
        </div>
      </div>

      <div className="flex justify-center">
        <div className="relative mb-4 flex w-full flex-wrap items-stretch">
          <div className="w-1/5 p-6 text-right font-bold">TITLE</div>
          <input
            className="w-4/5 p-6 rounded-r border border-solid border-neutral-300 shadow-md"
            name="title"
            type={"text"}
            value={todo.title}
            onChange={handleChangeTodo}
          ></input>
        </div>
      </div>

      <div className="flex justify-center">
        <div className="relative mb-4 flex w-full flex-wrap items-stretch">
          <div className="w-1/5 p-6 text-right font-bold">DUEDATE</div>
          <input
            className="w-4/5 p-6 rounded-r border border-solid border-neutral-300 shadow-md"
            name="dueDate"
            type={"date"}
            value={todo.dueDate}
            onChange={handleChangeTodo}
          ></input>
        </div>
      </div>

      <div className="flex justify-center">
        <div className="relative mb-4 flex w-full flex-wrap items-stretch">
          <div className="w-1/5 p-6 text-right font-bold">COMPLETE</div>
          <select
            name="status"
            className="border-solid border-2 rounded m-1 p-2"
            onChange={handleChangeTodoComplete}
            value={todo.complete ? "Y" : "N"}
          >
            <option value="Y">Completed</option>
            <option value="N">Not Yet</option>
          </select>
        </div>
      </div>

      <div className="flex justify-end p-4">
        <button
          type="button"
          className="inline-block rounded p-4 m-2 text-xl w-32  text-white bg-red-500"
          onClick={handleClickDelete}
        >
          Delete
        </button>
        <button
          type="button"
          className="rounded p-4 m-2 text-xl w-32 text-white bg-blue-500"
          onClick={handleClickModify}
        >
          Modify
        </button>
      </div>
    </div>
  );
};

export default ModifyComponent;

* 브라우저를 이용해서 수정 작업을 테스트해 보면 모달창이 보인 후에 조회 화면으로 이동하는 것을 확인할 수 있다. 이동시에 기존의 쿼리스트링 역시 동일하게 유지된다.

* 삭제의 경우 모달창이 닫힌 후에 목록 화면으로 이동하게 된다. 이 경우에도 쿼리스트링이 유지된다.