-
Notifications
You must be signed in to change notification settings - Fork 14
[배진한] Sprint10 #29
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: express-배진한
Are you sure you want to change the base?
The head ref may contain hidden characters: "express-\uBC30\uC9C4\uD55C-sprint10"
[배진한] Sprint10 #29
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| 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) => { | ||
| const { offset = 0, limit = 10, order = 'recent' } = req.query; | ||
| const offsetNum = parseInt(offset); | ||
| const limitNum = parseInt(limit); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 위 코드들은 request로부터 가져온 파라미터들을 검증(validation)하는 단계를 거칩니다. 이러한 부분은 레이어 까지는 아니더라도 별도의 함수를 통해서 각 파라미터가 앞으로 사용될 스키마(도메인모델)에 어떤 타입에 매핑되는지 확인하며 타입 또는 값에대한 검증을 진행할 수 있습니다. 아래의 거의 모든 함수들이 이러한 과정을 거칠거에요. 그렇다면 이부분은 검증이라는 단계로써 함수로 따로 구성할 수 있고 그함수는 입력받은 값을 검증하는 즉 같은 입력에 같은 return을 낼 수 있는 순수함수로 구성될 수 있습니다. |
||
|
|
||
| const orderBy = orderByFunction(order); | ||
|
|
||
| const products = await prisma.product.findMany({ | ||
| orderBy, | ||
| skip: offsetNum, // skip으로 offset설정 | ||
| take: limitNum, // take으로 limit설정 | ||
| }); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 위 부분은 prisma(ORM혹은 다른형태의 DB일수도 있습니다.) db에 직접 접근하여 값을 가져오는 query단계로 볼 수 있습니다. 때문에 이부분은 store(다른 이름이여도 됩니다)에 관련된 레이어로 분리하여 처리할 수 있습니다. 그럼 여기서는 DB에 접근하는 코드들만 모여서 모듈화가 될 것이고, 앞으로 이 함수는 DB에 관련된 코드를 제어하는 책임으로 분리가 될 수 있습니다. |
||
| console.log("products, 여기긴", products); | ||
| const responseData = await productsPaginationHandler(products, offsetNum, limitNum); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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')); | ||
| 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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
express를 통한 api 라우트 구성부터 로직, db접근까지 하나의 함수와 하나의파일들로 구성해주신걸 확인할 수 있어요.
이러한 부분들은 아래 후술하는 방법처럼, 또한 제가이전에 한번 공유드린 것 처럼 layer를 분리해서 파일을 정리하고, 하나의 함수가 하나의 기능만 담당하도록 정리할 수 있습니다. 이러한 방법은 함수의 책임을 좁히고, 코드를 모듈화 하며, 레이어와 레이어사이에 인터페이스를 두고 하나의 레이어가 다른 스펙으로 변경되어도 불변성과 무결성을 유지할 수 있는 방법을 아키텍처레벨에서 제어할 수 있습니다.