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 에러남
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();
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;
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;
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;
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;
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;