Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
428 changes: 428 additions & 0 deletions sprint10/.gitignore

Large diffs are not rendered by default.

291 changes: 291 additions & 0 deletions sprint10/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,291 @@
import * as dotenv from 'dotenv';
dotenv.config();
import express from "express";
import { PrismaClient, Prisma } from "@prisma/client";
import { assert } from "superstruct";
import {
CreateProduct,
PatchProduct,
CreateArticle,
PatchArticle,
CreateComment,
PatchComment
} from "./structs.js";
import asyncHandler from "./asyncHandlerFunction.js";
import orderByFunction from "./orderByFunction.js";
import {
productsPaginationHandler,
articlesPaginationHandler
} from "./paginationHandler.js";

const prisma = new PrismaClient();

const app = express();
app.use(express.json());

/** Products Routes **/

// 상품 목록 조회
app.get('/products', asyncHandler(async (req, res) => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

express를 통한 api 라우트 구성부터 로직, db접근까지 하나의 함수와 하나의파일들로 구성해주신걸 확인할 수 있어요.

이러한 부분들은 아래 후술하는 방법처럼, 또한 제가이전에 한번 공유드린 것 처럼 layer를 분리해서 파일을 정리하고, 하나의 함수가 하나의 기능만 담당하도록 정리할 수 있습니다. 이러한 방법은 함수의 책임을 좁히고, 코드를 모듈화 하며, 레이어와 레이어사이에 인터페이스를 두고 하나의 레이어가 다른 스펙으로 변경되어도 불변성과 무결성을 유지할 수 있는 방법을 아키텍처레벨에서 제어할 수 있습니다.

const { offset = 0, limit = 10, order = 'recent' } = req.query;
const offsetNum = parseInt(offset);
const limitNum = parseInt(limit);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  const { offset = 0, limit = 10, order = 'recent' } = req.query;
  const offsetNum = parseInt(offset);
  const limitNum = parseInt(limit);

위 코드들은 request로부터 가져온 파라미터들을 검증(validation)하는 단계를 거칩니다. 이러한 부분은 레이어 까지는 아니더라도 별도의 함수를 통해서 각 파라미터가 앞으로 사용될 스키마(도메인모델)에 어떤 타입에 매핑되는지 확인하며 타입 또는 값에대한 검증을 진행할 수 있습니다. 아래의 거의 모든 함수들이 이러한 과정을 거칠거에요.

그렇다면 이부분은 검증이라는 단계로써 함수로 따로 구성할 수 있고 그함수는 입력받은 값을 검증하는 즉 같은 입력에 같은 return을 낼 수 있는 순수함수로 구성될 수 있습니다.


const orderBy = orderByFunction(order);

const products = await prisma.product.findMany({
orderBy,
skip: offsetNum, // skip으로 offset설정
take: limitNum, // take으로 limit설정
});
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

위 부분은 prisma(ORM혹은 다른형태의 DB일수도 있습니다.) db에 직접 접근하여 값을 가져오는 query단계로 볼 수 있습니다.
여기선 prisma 에 접근했지만, DB에 접근한다면 connection과 같은 값들을 추가로 제어해야 할 수도 있어요.

때문에 이부분은 store(다른 이름이여도 됩니다)에 관련된 레이어로 분리하여 처리할 수 있습니다. 그럼 여기서는 DB에 접근하는 코드들만 모여서 모듈화가 될 것이고, 앞으로 이 함수는 DB에 관련된 코드를 제어하는 책임으로 분리가 될 수 있습니다.
즉 이 함수에서는 DB와 관련된 로직만 작성한다. 라는 의미로 이해하실 수 있습니다.

console.log("products, 여기긴", products);
const responseData = await productsPaginationHandler(products, offsetNum, limitNum);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

위 부분은 결국 가져온 값들을 페이지네이션을 시켜주기위한 단계로 보여집니다. 여기도 마찬가지로 레이어까지는 아니지만 별도의 함수로 분리해서 페이지네이션만을 담당하는 함수모듈로 분리할 수 있습니다. 마찬가지로 여기서도 페이지네이션만 담당하는 함수이니 다른것들은 신경쓰지 않는 방식으로 코드를 작성해도 무방합니다.

이미 함수로 나눠주신 부분이고, 그렇기 때문에 나뉘어진 코드는 후에 테스트 코드를 작성하거나, 오류(페이지네이션에 문제가 있어요)등이 발생했을때 코드의 어느부분을 확인하면 되는지 명확한 분리가 이루어질 수 있습니다.

res.send(responseData);
}));

// 상품 조회
app.get('/products/:productId', asyncHandler(async (req, res) => {
const { productId } = req.params;
const products = await prisma.product.findUniqueOrThrow({
where: { id: productId },
});
if (products) {
res.send(products);
} else {
res.status(404).send({ message: '상품을 찾을 수 없습니다.' })
}
}));

// 상품 생성
app.post('/products', asyncHandler(async (req, res) => {
try {
assert(req.body, CreateProduct);
const newProduct = await prisma.product.create({
data: req.body,
})
console.log(newProduct);
res.status(201).send(newProduct);
} catch (error) {
res.status(400).send('Bad Request: ' + error.message);
}
}));

// 상품 수정
app.patch('/products/:productId', asyncHandler(async (req, res) => {
const { productId } = req.params;
assert(req.body, PatchProduct);
const products = await prisma.product.update({
where: { id: productId },
data: req.body,
});
console.log(products);
res.send(products);
}))

// 상품 삭제
app.delete('/products/:productId', asyncHandler(async (req, res) => {
const productId = req.params.productId;
const product = await prisma.product.delete({
where: { id: productId },
});
if (product) {
res.sendStatus(204);
} else {
res.status(404).send({ message: 'id를 확인해주세요.' })
}
}))

/** Articles Routes **/

// 게시글 생성
app.post('/articles', asyncHandler(async (req, res) => {
try {
assert(req.body, CreateArticle);
const { title, content } = req.body;
const nweArticle = await prisma.article.create({
data: {
title,
content,
}
})
console.log(nweArticle);
res.status(201).send(nweArticle);
} catch (error) {
res.status(400).send('Bad Request: ' + error.message);
}
}))

// 게시글 조회
app.get('/articles/:articleId', asyncHandler(async (req, res) => {
const { articleId } = req.params;
const articles = await prisma.article.findUniqueOrThrow({
where: { id: articleId },
});
if (articles) {
res.send(articles);
} else {
res.status(404).send({ message: '상품을 찾을 수 없습니다.' })
}
}))

// 게시글 수정
app.patch('/articles/:articleId', asyncHandler(async (req, res) => {
const { articleId } = req.params;
assert(req.body, PatchArticle);
const articles = await prisma.article.update({
where: { id: articleId },
data: req.body,
});
console.log(articles);
res.send(articles);
}))

// 게시글 목록 조회
app.get('/articles', asyncHandler(async (req, res) => {
const { keyword, offset = 0, limit = 10, order = 'recent' } = req.query;
const offsetNum = parseInt(offset);
const limitNum = parseInt(limit);

const orderBy = orderByFunction(order);

if (keyword) {
const articles = await prisma.article.findMany({
where: {
OR: [
{ title: { contains: keyword, mode: 'insensitive' } },
{ content: { contains: keyword, mode: 'insensitive' } },
],
},
orderBy,
skip: offsetNum,
take: limitNum,
})
res.send(articles);
return;
}

const articles = await prisma.article.findMany({
orderBy,
skip: offsetNum,
take: limitNum,
})

const responseData = await articlesPaginationHandler(articles, offsetNum, limitNum);
res.send(responseData);
}))

// 게시글 삭제
app.delete('/articles/:articleId', asyncHandler(async (req, res) => {
const { articleId } = req.params;
const article = await prisma.article.delete({
where: { id: articleId },
});
if (article) {
res.sendStatus(204);
} else {
res.status(404).send({ message: 'id를 확인해주세요.' })
}
}))

// 게시글 댓글 목록 조회
app.get('/articles/:articleId/comments', asyncHandler(async (req, res) => {
const { articleId } = req.params;
const { cursor, limit = 10, order = 'recent' } = req.query;

const limitNum = parseInt(limit);

const orderBy = orderByFunction(order);
const comments = await prisma.comment.findMany({
where: {
articleId,
},
orderBy,
take: limitNum,
skip: cursor ? 1 : 0,
cursor: cursor ? { id: cursor } : undefined,
});

const lastComment = comments[comments.length - 1];
const nextCursor = lastComment ? lastComment.id : null;

res.send({ comments, nextCursor });
// send() 메서드를 사용할 때 인자의 값이 복수일 때는 객체로 전달해야 한다.
}))

/** Comments Routes **/

// 게시글 댓글 등록
app.post('/articles/:articleId/comments', asyncHandler(async (req, res) => {
try {
assert(req.body, CreateComment);
const { articleId } = req.params;
const { content } = req.body;
const newComment = await prisma.comment.create({
data: {
content,
article: {
connect: { id: articleId },
},
}
})
console.log(newComment);
res.status(201).send(newComment);
} catch (error) {
res.status(400).send('Bad Request: ' + error.message);
}
}))

// 게시글 댓글 수정
app.patch('/comments/:commentId', asyncHandler(async (req, res) => {
assert(req.body, PatchComment);
const { commentId } = req.params;
const { content } = req.body;
const comments = await prisma.comment.update({
where: { id: commentId },
data: { content },
});
console.log(comments);
res.send(comments);
}))

// 게시글 댓글 삭제
app.delete('/comments/:commentId', asyncHandler(async (req, res) => {
const { commentId } = req.params;
const comment = await prisma.comment.delete({
where: { id: commentId },
});
if (comment) {
res.sendStatus(204);
} else {
res.status(404).send({ message: 'id를 확인해주세요.' })
}
}))

// 모드 게시글 댓글 목록 조회
app.get('/comments', asyncHandler(async (req, res) => {
const { cursor, limit = 10, order = 'recent' } = req.query;

const limitNum = parseInt(limit);

const orderBy = orderByFunction(order);
const totalArticleComments = await prisma.comment.count();
const comments = await prisma.comment.findMany({
orderBy,
take: limitNum,
skip: cursor ? 1 : limitNum,
cursor: cursor ? { id: cursor } : undefined,
})

const lastComment = comments[comments.length - 1];
const nextCursor = lastComment ? lastComment.id : null;
const totalPage = Math.ceil(totalArticleComments / limitNum);

res.send({
totalArticleComments,
comments,
nextCursor,
totalPage
});
}))

app.listen(process.env.PORT || 8000, () => console.log('Server Started'));
26 changes: 26 additions & 0 deletions sprint10/asyncHandlerFunction.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Prisma } from "@prisma/client";

// 에러 처리 함수
function asyncHandler(handler) {
return async function (req, res) {
try {
await handler(req, res);
} catch (e) {
if (
e.name === 'StructureError' ||
e instanceof Prisma.PrismaClientValidationError
) {
res.status(400).send({ message: e.message });
} else if (
e instanceof Prisma.PrismaClientKnownRequestError &&
e.code === 'P2025'
) {
res.status(404).send({ message: 'Cannot find given id.' });
} else {
res.status(500).send({ message: e.message });
}
}
}
}

export default asyncHandler;
Loading
Loading