nestjs로 todolist 만들기 - 2. frontend with react

eugene's blog·2021년 8월 18일
0
post-thumbnail

backend port number 변경(3000 -> 3010)

main.ts

import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { AppModule } from './app.module';

declare const module: any;

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  // read
  app.useGlobalPipes(new ValidationPipe());

  const config = new DocumentBuilder()
    .setTitle('Todolist API')
    .setDescription('This is todolist API.')
    .setVersion('1.0')
    .addCookieAuth('connect.sid')
    .build();

  const document = SwaggerModule.createDocument(app, config);
  SwaggerModule.setup('api', app, document);

  await app.listen(3010);

  if (module.hot) {
    module.hot.accept();
    module.hot.dispose(() => app.close());
  }
}
bootstrap();
$ npx create-react-app frontend --template=typescript

.eslintrc.js & .prettierrc 파일 frontend 폴더에 붙여넣기

$ npm i -D eslint prettier eslint-config-prettier eslint-plugin-prettier @typescript-eslint/eslint-plugin@latest @typescript-eslint/parser@latest

lint 규칙 바뀌었으니 ctrl+s 한번씩

$ npm i axios

components/Todolist.tsx

import React, { useEffect, FC } from 'react';
import axios from 'axios';

const Todolist: FC = () => {
  useEffect(() => {
    const getTodos = async () => {
      try {
        const response = await axios.get('http://localhost:3010/todo');
        console.log(response);
      } catch (error) {
        console.error(error);
      }
    };

    getTodos();
  }, []);

  return <div>Todolist</div>;
};

export default Todolist;

axios로 요청하면 CORS 에러남

CORS


backend에서 풀어줘야함(접근 허용)

(backend)main.ts

import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { AppModule } from './app.module';

declare const module: any;

async function bootstrap() {
  const app = await NestFactory.create(AppModule, {
	// CORS 허용
    cors: {
      origin: 'http://localhost:3000',
      credentials: true,
    },
  });
  // read
  app.useGlobalPipes(new ValidationPipe());

  const config = new DocumentBuilder()
    .setTitle('Todolist API')
    .setDescription('This is todolist API.')
    .setVersion('1.0')
    .addCookieAuth('connect.sid')
    .build();

  const document = SwaggerModule.createDocument(app, config);
  SwaggerModule.setup('api', app, document);

  await app.listen(3010);

  if (module.hot) {
    module.hot.accept();
    module.hot.dispose(() => app.close());
  }
}
bootstrap();

R(Read)


components/todolist.tsx

import React, { useState, useEffect, FC } from 'react';
import axios from 'axios';

interface Itodo {
  id: number;
  createdAt: Date;
  updatedAt: Date;
  title: string;
  desc: string;
  isComplete: boolean;
}

const Todolist: FC = () => {
  const [todos, setTodos] = useState<Itodo[]>([]);

  useEffect(() => {
    const getTodos = async () => {
      try {
        const response = await axios.get('http://localhost:3010/todo');

        if (response.statusText === 'OK') {
          setTodos(response.data);
        }
      } catch (error) {
        console.error(error);
      }
    };

    getTodos();
  }, []);

  return (
    <div>
      {todos.map((todo) => {
        return (
          <li key={todo.id}>
            {todo.id} - {todo.title} - {todo.desc}
          </li>
        );
      })}
    </div>
  );
};

export default Todolist;

C(Create)


components/AddTodo.tsx

import React, { FC } from 'react';

const AddTodo: FC = () => {
  return <div>AddTodo</div>;
};

export default AddTodo;

components/Todolist.tsx

import React, { useState, useEffect, FC } from 'react';
import axios from 'axios';
import AddTodo from './AddTodo';

interface Itodo {
  id: number;
  createdAt: Date;
  updatedAt: Date;
  title: string;
  desc: string;
  isComplete: boolean;
}

const Todolist: FC = () => {
  const [todos, setTodos] = useState<Itodo[]>([]);

  useEffect(() => {
    const getTodos = async () => {
      try {
        const response = await axios.get('http://localhost:3010/todo');

        if (response.statusText === 'OK') {
          setTodos(response.data);
        }
      } catch (error) {
        console.error(error);
      }
    };

    getTodos();
  }, []);

  return (
    <div>
      <AddTodo />
      {todos.map((todo) => {
        return (
          <li key={todo.id}>
            {todo.id} - {todo.title} - {todo.desc}
          </li>
        );
      })}
    </div>
  );
};

export default Todolist;

components/AddTodo.tsx

import React, { ChangeEvent, FC, useState } from 'react';

const AddTodo: FC = () => {
  const [addTitle, setAddTitle] = useState<string>('');
  const [addDesc, setAddDesc] = useState<string>('');

  const onChangeAddTitle = (e: ChangeEvent<HTMLInputElement>) => {
    const { value } = e.target;

    setAddTitle(value);
  };

  const onChangeAddDesc = (e: ChangeEvent<HTMLInputElement>) => {
    const { value } = e.target;

    setAddDesc(value);
  };

  return (
    <form>
      <input type="text" value={addTitle} onChange={onChangeAddTitle} />
      <input type="text" value={addDesc} onChange={onChangeAddDesc} />
      <input type="submit" value="Add" />
    </form>
  );
};

export default AddTodo;

R(Read)


SWR

  • backend와 sync맞춰주는 것
  • get에 특화된 axios
$ npm i swr

components/todolist.tsx

import React, { FC } from 'react';
import axios from 'axios';
import useSWR from 'swr';
import AddTodo from './AddTodo';

export interface Itodo {
  id: number;
  createdAt: Date;
  updatedAt: Date;
  title: string;
  desc: string;
  isComplete: boolean;
}

const Todolist: FC = () => {
  const fetcher = async (url: string) => {
    try {
      const response = await axios.get(url);

      return response.data;
    } catch (error) {
      console.error(error);
    }
  };

  const { data, error, mutate } = useSWR<Itodo[]>(
    `${process.env.REACT_APP_BACK_URL}/todo`,
    fetcher,
  );

  if (!data) return <div>Loading...</div>;
  if (error) return <div>Error</div>;

  return (
    <div>
      <AddTodo mutate={mutate} />
      {data.map((todo) => {
        return (
          <li key={todo.id}>
            {todo.id} - {todo.title} - {todo.desc}
          </li>
        );
      })}
    </div>
  );
};

export default Todolist;

components/addTodo.tsx

import React, { ChangeEvent, FC, FormEvent, useState } from 'react';
import axios from 'axios';
import { MutatorCallback } from 'swr/dist/types';
import { Itodo } from './Todolist';

interface AddTodoProps {
  mutate: (
    data?: Itodo[] | Promise<Itodo[]> | MutatorCallback<Itodo[]> | undefined,
    shouldRevalidate?: boolean | undefined,
  ) => Promise<Itodo[] | undefined>;
}

const AddTodo: FC<AddTodoProps> = ({ mutate }) => {
  const [addTitle, setAddTitle] = useState<string>('');
  const [addDesc, setAddDesc] = useState<string>('');

  const onChangeAddTitle = (e: ChangeEvent<HTMLInputElement>) => {
    const { value } = e.target;

    setAddTitle(value);
  };

  const onChangeAddDesc = (e: ChangeEvent<HTMLInputElement>) => {
    const { value } = e.target;

    setAddDesc(value);
  };

  const onSubmitAddTodo = async (e: FormEvent<HTMLFormElement>) => {
    try {
      e.preventDefault();

      const response = await axios.post(
        `${process.env.REACT_APP_BACK_URL}/todo`,
        {
          title: addTitle,
          desc: addDesc,
        },
      );
      if (response.statusText === 'Created') {
        setAddTitle('');
        setAddDesc('');
        mutate();
      }
    } catch (error) {
      console.error(error);
    }
  };

  return (
    <form onSubmit={onSubmitAddTodo}>
      <input type="text" value={addTitle} onChange={onChangeAddTitle} />
      <input type="text" value={addDesc} onChange={onChangeAddDesc} />
      <input type="submit" value="Add" />
    </form>
  );
};

export default AddTodo;

D(delete)


component 분리

components/Todolist.tsx

import React, { FC } from 'react';
import axios from 'axios';
import useSWR from 'swr';
import AddTodo from './AddTodo';
import Todo from './Todo';

export interface Itodo {
  id: number;
  createdAt: Date;
  updatedAt: Date;
  title: string;
  desc: string;
  isComplete: boolean;
}

const Todolist: FC = () => {
  const fetcher = async (url: string) => {
    try {
      const response = await axios.get(url);

      return response.data;
    } catch (error) {
      console.error(error);
    }
  };

  const { data, error, mutate } = useSWR<Itodo[]>(
    `${process.env.REACT_APP_BACK_URL}/todo`,
    fetcher,
  );

  if (!data) return <div>Loading...</div>;
  if (error) return <div>Error</div>;

  return (
    <div>
      <AddTodo mutate={mutate} />
      {data.map((todo) => {
        return (
          <Todo
            key={todo.id}
            id={todo.id}
            title={todo.title}
            desc={todo.desc}
          />
        );
      })}
    </div>
  );
};

export default Todolist;

components/Todo.tsx

import React, { FC } from 'react';

interface TodoProps {
  id: number;
  title: string;
  desc: string;
}

const Todo: FC<TodoProps> = ({ id, title, desc }) => {
  return (
    <li>
      {id} - {title} - {desc}
    </li>
  );
};

export default Todo;

components/DeleteTodo.tsx

import axios from 'axios';
import React, { FC } from 'react';
import { AddTodoProps } from './AddTodo';

interface DeleteTodoProps extends AddTodoProps {
  id: number;
}

const DeleteTodo: FC<DeleteTodoProps> = ({ id, mutate }) => {
  const onClickDeleteTodo = async () => {
    try {
      const response = await axios.delete(
        `${process.env.REACT_APP_BACK_URL}/todo/${id}`,
      );

      if (response.statusText === 'OK') {
        mutate();
      }
    } catch (error) {
      console.error(error);
    }
  };

  return <button onClick={onClickDeleteTodo}>delete</button>;
};

export default DeleteTodo;

components/Todo.tsx

import React, { FC } from 'react';
import { mutate } from 'swr';
import { AddTodoProps } from './AddTodo';
import DeleteTodo from './DeleteTodo';

interface TodoProps extends AddTodoProps {
  id: number;
  title: string;
  desc: string;
}

const Todo: FC<TodoProps> = ({ id, title, desc, mutate }) => {
  return (
    <li>
      {id} - {title} - {desc} <DeleteTodo id={id} mutate={mutate} />
    </li>
  );
};

export default Todo;

components/Todolist.tsx

import React, { FC } from 'react';
import axios from 'axios';
import useSWR from 'swr';
import AddTodo from './AddTodo';
import Todo from './Todo';

export interface Itodo {
  id: number;
  createdAt: Date;
  updatedAt: Date;
  title: string;
  desc: string;
  isComplete: boolean;
}

const Todolist: FC = () => {
  const fetcher = async (url: string) => {
    try {
      const response = await axios.get(url);

      return response.data;
    } catch (error) {
      console.error(error);
    }
  };

  const { data, error, mutate } = useSWR<Itodo[]>(
    `${process.env.REACT_APP_BACK_URL}/todo`,
    fetcher,
  );

  if (!data) return <div>Loading...</div>;
  if (error) return <div>Error</div>;

  return (
    <div>
      <AddTodo mutate={mutate} />
      {data.map((todo) => {
        return (
          <Todo
            key={todo.id}
            id={todo.id}
            title={todo.title}
            desc={todo.desc}
            mutate={mutate}
          />
        );
      })}
    </div>
  );
};

export default Todolist;

U(update)


interface vs type

components/UpdateTodo.tsx

import React, { FC, useState } from 'react';
import { TodoProps } from './Todo';

type UpdateTodoProps = TodoProps;

const UpdateTodo: FC<UpdateTodoProps> = ({ id, title, desc, mutate }) => {
  const [updateToggle, setUpdateToggle] = useState<boolean>(false);

  const onClickUpdateToggle = () => {
    setUpdateToggle(!updateToggle);
  };

  return (
    <>
      {updateToggle ? (
        <div>update</div>
      ) : (
        <div>
          {id} - {title} - {desc}
        </div>
      )}
      <button onClick={onClickUpdateToggle}>
        {updateToggle ? 'Cancel' : 'Update'}
      </button>
    </>
  );
};

export default UpdateTodo;

components/todo.tsx

import React, { FC } from 'react';
import { mutate } from 'swr';
import { AddTodoProps } from './AddTodo';
import DeleteTodo from './DeleteTodo';
import UpdateTodo from './UpdateTodo';

export interface TodoProps extends AddTodoProps {
  id: number;
  title: string;
  desc: string;
}

const Todo: FC<TodoProps> = ({ id, title, desc, mutate }) => {
  return (
    <li>
      <UpdateTodo id={id} title={title} desc={desc} mutate={mutate} />
      <DeleteTodo id={id} mutate={mutate} />
    </li>
  );
};

export default Todo;

-> todolist 내림차순으로 가져오기
backend/todo/todo.service.ts

import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { CreateTodoDto } from './dtos/createTodo.dto';
import { UpdateTodoDto } from './dtos/updateTodo.dto';
import { Todo } from './entities/todo.entity';

@Injectable()
export class TodoService {
  constructor(
    @InjectRepository(Todo) private readonly todoRepository: Repository<Todo>,
  ) {}

  async createTodo(createTodoDto: CreateTodoDto) {
    return await this.todoRepository.save(createTodoDto);
  }

  async getTodos() {
    return await this.todoRepository.find({
      order: { createdAt: 'DESC' },
    });
  }

  async updateTodo(param, updateTodoDto: UpdateTodoDto) {
    const todo = await this.todoRepository.findOne({
      where: {
        id: param.todoId,
      },
    });

    if (!updateTodoDto.title && !updateTodoDto.desc) {
      //400 error
      throw new HttpException(
        '최소 하나의 값이 필요합니다',
        HttpStatus.FORBIDDEN,
      );
    }

    todo.title = updateTodoDto.title;
    todo.desc = updateTodoDto.desc;

    return this.todoRepository.save(todo);
  }

  async deleteTodo(param: { todoId: string }) {
    return await this.todoRepository.delete(param.todoId);
  }

  async toggleComplete(param: { todoId: string }) {
    const todo = await this.todoRepository.findOne({
      where: {
        id: +param.todoId,
      },
    });

    todo.isComplete = !todo.isComplete;

    return await this.todoRepository.save(todo);
  }
}

components/updateTodo.tsx

import axios from 'axios';
import React, { ChangeEvent, FC, FormEvent, useState } from 'react';
import { TodoProps } from './Todo';

type UpdateTodoProps = TodoProps;

const UpdateTodo: FC<UpdateTodoProps> = ({ id, title, desc, mutate }) => {
  const [updateToggle, setUpdateToggle] = useState<boolean>(false);
  const [updateTitle, setUpdateTitle] = useState<string>(title);
  const [updateDesc, setUpdateDesc] = useState<string>(title);

  const onClickUpdateToggle = () => {
    setUpdateToggle(!updateToggle);
  };

  const onChangeTitle = (e: ChangeEvent<HTMLInputElement>) => {
    const { value } = e.target;
    setUpdateTitle(value);
  };

  const onChangeDesc = (e: ChangeEvent<HTMLInputElement>) => {
    const { value } = e.target;
    setUpdateDesc(value);
  };

  const onSubmitUpdateTodo = async (e: FormEvent<HTMLFormElement>) => {
    try {
      e.preventDefault();

      if (!updateTitle || !updateDesc) {
        return;
      }

      const response = await axios.put(
        `${process.env.REACT_APP_BACK_URL}/todo/${id}`,
        {
          title: updateTitle,
          desc: updateDesc,
        },
      );

      if (response.statusText === 'OK') {
        setUpdateToggle(false);
        mutate();
      }
    } catch (error) {
      console.error(error);
    }
  };

  return (
    <>
      {updateToggle ? (
        <form onSubmit={onSubmitUpdateTodo}>
          <label>Title : </label>
          <input type="text" value={updateTitle} onChange={onChangeTitle} />
          <br />
          <label>Desc : </label>
          <input type="text" value={updateDesc} onChange={onChangeDesc} />
          <br />
          <input type="submit" value="Confirm" />
        </form>
      ) : (
        <div>
          {id} - {title} - {desc}
        </div>
      )}
      <button onClick={onClickUpdateToggle}>
        {updateToggle ? 'Cancel' : 'Update'}
      </button>
    </>
  );
};

export default UpdateTodo;
profile
매일 노력하는 개발자 김유진입니다.

0개의 댓글