풀 스택 React 및 Node.js - CRUD

마무리합시다.

node-server 폴더에서 note.model.js를 다음과 같이 편집합니다.

const { prisma } = require("./db")

async function getNotes() {
  return prisma.note.findMany()
}

async function getNote(id) {
  return prisma.note.findUnique({ where: { id } })
}

async function createNote(
  note
) {
  return prisma.note.create({
    data: note
  })
}

async function updateNote(
  id, note
) {
  return prisma.note.update({
    data: note,
    where: {
      id
    }
  })
}

async function deleteNote(
  id
) {
  return prisma.note.delete({
    where: {
      id
    }
  })
}

module.exports = {
  getNotes,
  getNote,
  createNote,
  updateNote,
  deleteNote,
}


node-server 폴더에서 note.controller.js를 다음과 같이 편집합니다.

const authorRepo = require('../models/author.model');
const noteRepo = require('../models/note.model');

async function getNotes(req, res) {
  const notes = await noteRepo.getNotes();

  res.json({
    notes
  });
}

async function getNote(req, res) {
  const {id} = req.params;
  const note = await noteRepo.getNote(id);
  const { authorId, ...noteRest } = note;
  const { username } = await authorRepo.getAuthor(authorId);

  res.json({ note: {
      ...noteRest,
      author: username
    }
  });
}

async function retrieveOrCreateAuthor(username) {
  let author = await authorRepo.getAuthorByName(username);
  if (author === null) {
    author = await authorRepo.createAuthor({
      username
    })
  }

  return author
}

async function postNote(req, res) {
  const {body} = req;
  const {title, content, author, lang, isLive, category} = body;

  try {
    const noteAuthor = await retrieveOrCreateAuthor(author);

    const note = await noteRepo.createNote({
      title,
      content,
      lang,
      isLive,
      category,
      authorId: noteAuthor.id
    })

    res
      .status(200)
      .json({
        note
      })
  } catch (e) {
    console.error(e);
    res.status(500).json({error: "Something went wrong"})
  }
}

async function putNote(req, res) {
  const {body} = req;
  const {id, title, content, author, lang, isLive, category} = body;

  try {
  const noteAuthor = await retrieveOrCreateAuthor(author);
  const note = await noteRepo.updateNote(id, {
    title,
    content,
    lang,
    isLive,
    category,
    authorId: noteAuthor.id
  })

  res
    .status(200)
    .json({
      note
    })
  } catch (e) {
    console.error(e);
    res.status(500).json({error: "Something went wrong"})
  }
}

async function deleteNote(req, res) {
  const {body} = req;
  const {id} = body;

  try {
    await noteRepo.deleteNote(id)

    res
      .status(200).send()
  } catch (e) {
    console.error(e);
    res.status(500).json({error: "Something went wrong"})
  }
}

module.exports = {
  getNotes,
  getNote,
  postNote,
  putNote,
  deleteNote,
}


node-server에서 route/index.js를 다음과 같이 편집합니다.

const express = require('express');
const noteRouter = express.Router();
const noteController = require('../controllers/note.controller');

noteRouter.get('/', noteController.getNotes);
noteRouter.get('/:id', noteController.getNote);
noteRouter.post('/', noteController.postNote);
noteRouter.put('/', noteController.putNote);
noteRouter.delete('/', noteController.deleteNote);

const routes = app => {
  app.use('/note', noteRouter);
};

module.exports = routes


서버 측에는 이제 기본 CRUD 작업에 필요한 모든 작업이 있습니다.

Create, Read, Update, Delete



지금 클라이언트와 서버를 실행해 보십시오. 양식에서 제출 버튼을 클릭하면 두 가지 문제가 있음을 알 수 있습니다. 첫째, 양식이 응답하지 않고 반복해서 클릭해도 어떤 일이 발생했는지 알 수 없습니다. 둘째, 서버 콘솔을 보면 오류가 있음을 알 수 있습니다.

Argument isLive: Got invalid value 'true' on prisma.createOneNote. Provided String, expected Boolean.


isLive는 부울이지만 Prisma에 문자열로 전송됩니다.

node-server index.js에서 다음을 사용합니다.

app.use(bodyParser.json());


이것은 실제로 구문 분석 중에 올바른 유형을 검색하므로 문제는 클라이언트에 있어야 합니다. onSubmit 핸들러에서 Form.js의 입력 제어 데이터를 수집할 때 항상 문자열을 반환하는 input.value를 사용합니다.

Form.js를 다음과 같이 편집합니다.

import React, {useState} from 'react';
import InputLabel from "./InputLabel";
import {isEmptyString, isNullOrUndefined, titleFromName} from "./strings";
import './form.css'

const Form = ({entity, onSubmitHandler, onDeleteHandler}) => {
  const [isSubmitting, setIsSubmitting] = useState(false);

  return (
    <form onSubmit={e => {
      setIsSubmitting(true);
      const form = e.target;
      const newEntity = Object.values(form).reduce((obj, field) => {
        const {name} = field;

        if (!isEmptyString(name)) {
          switch (typeof entity[name]) {
            case "number":
              obj[name] = field.valueAsNumber;
              break;
            case "boolean":
              obj[name] = field.value === 'true';
              break;
            default:
              obj[name] = field.value
          }
        }

        return obj
    }, {})
      onSubmitHandler(newEntity);

      e.stopPropagation();
      e.preventDefault()
    }}>
      <fieldset
        disabled={isSubmitting}
      >
      {
        Object.entries(entity).map(([entityKey, entityValue]) => {
          if (entityKey === "id") {
            return <input
              type="hidden"
              name="id"
              key="id"
              value={entityValue}
            />
          } else {
            return <InputLabel
              id={entityKey}
              key={entityKey}
              label={titleFromName(entityKey)}
              type={
                typeof entityValue === "boolean"
                  ? "checkbox"
                  : "text"
              }
              value={entityValue}
            />
          }
        })
      }
      </fieldset>
      <button
        type="submit"
        disabled={isSubmitting}
      >
        {
          isSubmitting ? 'Submitting' : 'Submit'
        }
      </button>
      {
        onDeleteHandler && !isNullOrUndefined(entity.id) && <button
          disabled={isSubmitting}
          onClick={() => {
            setIsSubmitting(true);
            onDeleteHandler(entity.id)
          }}
        >
          Delete
        </button>
      }
    </form>
  );
};

export default Form;


변경 사항:
  • 입력 컨트롤을 fieldset 태그로 래핑하여 사용자가 "제출"을 클릭하면 모든 컨트롤을 비활성화할 수 있습니다
  • .
  • switch 문을 사용하여 입력 값을 구문 분석하여 양식 작성에 사용하는 원래 항목의 유형과 일치하도록 합니다.

  • 양식을 다시 저장하면 버그가 수정되었음을 알 수 있습니다.

    나머지 CRUD 작업을 구현하기 전에 작은 리팩터링이 필요합니다. react-client에서 .env.development를 생성합니다.

    REACT_APP_URL_API=http://localhost:4011/
    


    useFetch.js를 생성합니다.

    import {useState, useEffect} from "react";
    
    export const getUrl = url => new URL(url, process.env.REACT_APP_URL_API).toString();
    
    function useFetch(url, skip) {
      const [data, setData] = useState({});
    
      useEffect( () => {
        const abortController = new AbortController();
    
        async function fetchData() {
          const fullUrl = getUrl(url);
          console.log('Fetching from: ' + fullUrl);
          try {
            const response = await fetch(fullUrl, {
              signal: abortController.signal,
            });
    
            if (response.ok) {
              console.log('Response received from server and is ok!')
              const res = await response.json();
    
              if (abortController.signal.aborted) {
                console.log('Abort detected, exiting!')
                return;
              }
    
              setData(res)
            }
          } catch(e) {
            console.log(e)
          }
        }
    
        !skip && fetchData()
    
        return () => {
          console.log('Aborting GET request.')
          abortController.abort();
        }
      }, [url, setData, skip])
    
      return data
    }
    
    export default useFetch
    


    현재 양식은 새 메모를 추가만 할 수 있으며 편집은 할 수 없습니다. 몇 가지 작업을 수행해야 합니다.
  • 모든 메모 나열
  • 메모 편집
  • 메모 추가
  • 메모 삭제

  • AddEditNote.js를 다음과 같이 리팩터링:

    import React from 'react';
    import {useParams, useNavigate} from "react-router-dom";
    import RenderData from "./RenderData";
    import Form from './Form';
    import useFetch, {getUrl} from "./useFetch";
    import {isNullOrUndefined} from "./strings";
    
    const AddEditNote = () => {
      const {noteId} = useParams();
      const {note = {
        title: '',
        content: '',
        lang: '',
        isLive: false,
        category: '',
        author: '',
      }} = useFetch('note/' + noteId, isNullOrUndefined(noteId));
      const navigate = useNavigate();
    
      return (
        <div>
          <RenderData
            data={note}
          />
          <Form
            entity={note}
            onSubmitHandler={async newNote => {
              console.log({newNote})
              const response = await fetch(getUrl('note'), {
                method: isNullOrUndefined(newNote.id) ? 'POST' : 'PUT',
                body: JSON.stringify(newNote),
                headers: {
                  'Content-Type': 'application/json'
                }
              });
    
              if (response.ok) {
                await response.json()
                navigate('/note-list')
              }
            }}
            onDeleteHandler={async (id) => {
              if (!isNullOrUndefined(id)) {
                await fetch(getUrl('note'), {
                  method: 'DELETE',
                  body: JSON.stringify({id}),
                  headers: {
                    'Content-Type': 'application/json'
                  }
                });
    
                navigate('/note-list')
              }
            }}
          />
        </div>
      );
    };
    
    export default AddEditNote;
    


    반응 클라이언트에서 TableList.js 만들기

    import React from 'react';
    import {titleFromName} from './strings';
    import './table-list.css';
    
    const TableList = ({
                         data,
                         title,
                         onClickHandler,
                         idField = 'id',
                         fieldFormatter = {},
                       }) => {
      if (!data || data.length === 0) {
        return null
      }
      const firstRow = data[0];
      const dataColumnNamesToRender = Object.getOwnPropertyNames(firstRow)
        .filter(propName => propName !== idField);
    
      const headerRow = dataColumnNamesToRender.map((propName, i) => <th
        key={i}
      >
        {
          titleFromName(propName)
        }
      </th>);
    
      return (
        <table>
          <caption>
            {
              title
            }
          </caption>
          <thead>
          <tr>
            {
              headerRow
            }
          </tr>
          </thead>
          <tbody>
          {
            data.map((dataRow, i) => <tr
              key={i}
              onClick={() => onClickHandler && onClickHandler(dataRow[idField])}
            >
              {
                dataColumnNamesToRender.map((dataColumnName, i) => <td
                  key={i}
                >
                  {
                    (fieldFormatter[dataColumnName] ?? (v => v))(dataRow[dataColumnName], dataRow)
                  }
                </td>)
              }
            </tr>)
          }
          </tbody>
        </table>
      );
    };
    
    export default TableList;
    


    react-client에서 table-list.css 생성

    table {
        margin: 12px;
        border-collapse: collapse;
    }
    
    th {
        color: white;
        padding: 8px;
        background-color: #444;
    }
    
    td {
        border-bottom: 1px solid #ddd;
        padding: 12px;
    }
    
    td a,
    td a:visited {
        color: black;
    }
    
    td:not(:last-child) {
        border-left:1px solid #ccc;
        border-right: 1px solid #ccc;
    }
    
    tr:nth-child(even) {
        background-color: #f1f1f1;
    }
    


    일반 양식 구성 요소와 마찬가지로 일반 데이터 목록 구성 요소입니다.

    반응 클라이언트에서 NoteList.js 만들기

    import React from 'react';
    import TableList from "./TableList";
    import {Link} from "react-router-dom";
    import useFetch from "./useFetch";
    
    const NoteList = () => {
      const {notes} = useFetch('note')
    
      return (
        <TableList
          data={notes}
          fieldFormatter={{
            title: (title, dataRow) => [
              <Link
                to={`/edit-note/${dataRow.id}`}
                key='1'
              >
                edit
              </Link>,
              <span key="2">
                &nbsp;{
                  title
                }
              </span>
            ],
            dateCreated: date => new Date(date).toLocaleString()
          }}
        />
      );
    };
    
    export default NoteList;
    


    이것은 TableList.js를 사용하여 메모를 나열합니다.

    마지막으로 App.js를 다음과 같이 변경합니다.

    import {
      Link,
      HashRouter as Router,
      Routes,
      Route,
    } from 'react-router-dom';
    import AddEditNote from "./AddEditNote";
    import NoteList from "./NoteList";
    import './App.css';
    
    function App() {
      return (
          <div className="App">
            <Router>
              <Routes>
                <Route exact path="/" element={
                  <ul>
                    <li>
                      <Link to="/note-list">List Notes</Link>
                    </li>
                    <li>
                      <Link to="/edit-note">Create Note</Link>
                    </li>
                  </ul>
                }/>
                <Route path="/note-list" element={<NoteList/>}/>
                <Route path="/edit-note" element={<AddEditNote/>}/>
                <Route path="/edit-note/:noteId" element={<AddEditNote/>}/>
              </Routes>
            </Router>
          </div>
      );
    }
    
    export default App;
    


    지금 실행하면 모든 기본 CRUD 작업이 작동합니다.

    축하합니다, 풀 스택.

    이 앱에는 양식 유효성 검사 및 날짜 처리, 드롭다운 목록과 같은 몇 가지 사항이 없습니다. 그러나 이것들은 쉽게 추가할 수 있어야 합니다...

    코드 저장소: Github Repository

    좋은 웹페이지 즐겨찾기