From 3e806fbf737b67497ed916a672a0bbe1137420ac Mon Sep 17 00:00:00 2001 From: Jinhan Date: Sun, 29 Dec 2024 01:45:11 +0900 Subject: [PATCH] =?UTF-8?q?chore:=20sprint10=20=EB=AF=B8=EC=85=98=20?= =?UTF-8?q?=EC=A4=80=EB=B9=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sprint10/.gitignore | 428 ++++++ sprint10/app.js | 291 ++++ sprint10/asyncHandlerFunction.js | 26 + sprint10/docs/sprint10.md | 97 ++ sprint10/http/articles.http | 44 + sprint10/http/comment.http | 21 + sprint10/http/products.http | 38 + sprint10/orderByFunction.js | 12 + sprint10/package-lock.json | 1313 +++++++++++++++++ sprint10/package.json | 27 + sprint10/paginationHandler.js | 30 + .../migration.sql | 31 + .../migration.sql | 11 + .../migration.sql | 2 + .../migration.sql | 8 + .../migration.sql | 3 + .../migration.sql | 34 + .../migration.sql | 2 + .../migration.sql | 2 + .../migration.sql | 2 + .../prisma/migrations/migration_lock.toml | 3 + sprint10/prisma/mocks/articlesMock.js | 127 ++ sprint10/prisma/mocks/commentsMock.js | 127 ++ sprint10/prisma/mocks/productsMock.js | 240 +++ sprint10/prisma/schema.prisma | 48 + sprint10/prisma/seed.js | 61 + sprint10/structs.js | 30 + 27 files changed, 3058 insertions(+) create mode 100644 sprint10/.gitignore create mode 100644 sprint10/app.js create mode 100644 sprint10/asyncHandlerFunction.js create mode 100644 sprint10/docs/sprint10.md create mode 100644 sprint10/http/articles.http create mode 100644 sprint10/http/comment.http create mode 100644 sprint10/http/products.http create mode 100644 sprint10/orderByFunction.js create mode 100644 sprint10/package-lock.json create mode 100644 sprint10/package.json create mode 100644 sprint10/paginationHandler.js create mode 100644 sprint10/prisma/migrations/20241116061602_add_product_email/migration.sql create mode 100644 sprint10/prisma/migrations/20241116094525_remove_product_email/migration.sql create mode 100644 sprint10/prisma/migrations/20241116130153_add_product_likes/migration.sql create mode 100644 sprint10/prisma/migrations/20241116131817_add_product_likes/migration.sql create mode 100644 sprint10/prisma/migrations/20241116155152_add_product_likes_opptional/migration.sql create mode 100644 sprint10/prisma/migrations/20241116174703_add_comment_model/migration.sql create mode 100644 sprint10/prisma/migrations/20241117113216_add_aritcle_likes/migration.sql create mode 100644 sprint10/prisma/migrations/20241117144233_add_comment_product_opptional/migration.sql create mode 100644 sprint10/prisma/migrations/20241117145605_add_comment_likes/migration.sql create mode 100644 sprint10/prisma/migrations/migration_lock.toml create mode 100644 sprint10/prisma/mocks/articlesMock.js create mode 100644 sprint10/prisma/mocks/commentsMock.js create mode 100644 sprint10/prisma/mocks/productsMock.js create mode 100644 sprint10/prisma/schema.prisma create mode 100644 sprint10/prisma/seed.js create mode 100644 sprint10/structs.js diff --git a/sprint10/.gitignore b/sprint10/.gitignore new file mode 100644 index 00000000..8310ca5a --- /dev/null +++ b/sprint10/.gitignore @@ -0,0 +1,428 @@ +# Created by https://www.toptal.com/developers/gitignore/api/node,visualstudiocode,yarn,react,reactnative,windows,macos +# Edit at https://www.toptal.com/developers/gitignore?templates=node,visualstudiocode,yarn,react,reactnative,windows,macos + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### macOS Patch ### +# iCloud generated files +*.icloud + +### Node ### +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +### Node Patch ### +# Serverless Webpack directories +.webpack/ + +# Optional stylelint cache + +# SvelteKit build / generate output +.svelte-kit + +### react ### +.DS_* +**/*.backup.* +**/*.back.* + +node_modules + +*.sublime* + +psd +thumb +sketch + +### ReactNative ### +# React Native Stack Base + +.expo +__generated__ + +### ReactNative.Node Stack ### +# Logs + +# Diagnostic reports (https://nodejs.org/api/report.html) + +# Runtime data + +# Directory for instrumented libs generated by jscoverage/JSCover + +# Coverage directory used by tools like istanbul + +# nyc test coverage + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) + +# Bower dependency directory (https://bower.io/) + +# node-waf configuration + +# Compiled binary addons (https://nodejs.org/api/addons.html) + +# Dependency directories + +# Snowpack dependency directory (https://snowpack.dev/) + +# TypeScript cache + +# Optional npm cache directory + +# Optional eslint cache + +# Optional stylelint cache + +# Microbundle cache + +# Optional REPL history + +# Output of 'npm pack' + +# Yarn Integrity file + +# dotenv environment variable files + +# parcel-bundler cache (https://parceljs.org/) + +# Next.js build output + +# Nuxt.js build / generate output + +# Gatsby files +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output + +# vuepress v2.x temp and cache directory + +# Docusaurus cache and generated files + +# Serverless directories + +# FuseBox cache + +# DynamoDB Local files + +# TernJS port file + +# Stores VSCode versions used for testing VSCode extensions + +# yarn v2 + +### ReactNative.macOS Stack ### +# General + +# Icon must end with two \r + +# Thumbnails + +# Files that might appear in the root of a volume + +# Directories potentially created on remote AFP share + +### ReactNative.Android Stack ### +# Gradle files +.gradle/ +build/ + +# Local configuration file (sdk path, etc) +local.properties + +# Log/OS Files + +# Android Studio generated files and folders +captures/ +.externalNativeBuild/ +.cxx/ +*.apk +output.json + +# IntelliJ +*.iml +.idea/ +misc.xml +deploymentTargetDropDown.xml +render.experimental.xml + +# Keystore files +*.jks +*.keystore + +# Google Services (e.g. APIs or Firebase) +google-services.json + +# Android Profiling +*.hprof + +### ReactNative.Xcode Stack ### +## User settings +xcuserdata/ + +## Xcode 8 and earlier +*.xcscmblueprint +*.xccheckout + +### ReactNative.Buck Stack ### +buck-out/ +.buckconfig.local +.buckd/ +.buckversion +.fakebuckversion + +### ReactNative.Gradle Stack ### +.gradle +**/build/ +!src/**/build/ + +# Ignore Gradle GUI config +gradle-app.setting + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar + +# Avoid ignore Gradle wrappper properties +!gradle-wrapper.properties + +# Cache of project +.gradletasknamecache + +# Eclipse Gradle plugin generated files +# Eclipse Core +.project +# JDT-specific (Eclipse Java Development Tools) +.classpath + +### ReactNative.Linux Stack ### +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +### VisualStudioCode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide + +### Windows ### +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +### yarn ### +# https://yarnpkg.com/getting-started/qa#which-files-should-be-gitignored + +.yarn/* +!.yarn/releases +!.yarn/patches +!.yarn/plugins +!.yarn/sdks +!.yarn/versions + +# if you are NOT using Zero-installs, then: +# comment the following lines +!.yarn/cache + +# and uncomment the following lines +# .pnp.* + +# End of https://www.toptal.com/developers/gitignore/api/node,visualstudiocode,yarn,react,reactnative,windows,macos \ No newline at end of file diff --git a/sprint10/app.js b/sprint10/app.js new file mode 100644 index 00000000..a4b4b648 --- /dev/null +++ b/sprint10/app.js @@ -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); + + const orderBy = orderByFunction(order); + + const products = await prisma.product.findMany({ + orderBy, + skip: offsetNum, // skip으로 offset설정 + take: limitNum, // take으로 limit설정 + }); + console.log("products, 여기긴", products); + const responseData = await productsPaginationHandler(products, offsetNum, limitNum); + 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')); \ No newline at end of file diff --git a/sprint10/asyncHandlerFunction.js b/sprint10/asyncHandlerFunction.js new file mode 100644 index 00000000..0fb2f469 --- /dev/null +++ b/sprint10/asyncHandlerFunction.js @@ -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; \ No newline at end of file diff --git a/sprint10/docs/sprint10.md b/sprint10/docs/sprint10.md new file mode 100644 index 00000000..bab97af0 --- /dev/null +++ b/sprint10/docs/sprint10.md @@ -0,0 +1,97 @@ +## 📋 스프린트 미션 요구사항 + +## 요구사항 + +### 기본 요구사항 + +#### 공통 +- [ ] Github에 위클리 미션 PR을 만들어 주세요. +- [ ] React.js 혹은 Next.js를 사용해 진행합니다. +- [ ] RESTful를 설계하고 백엔드 코드를 변경하세요. + - [ ] (풀스택) 설계한 백엔드 코드에 맞게 프론트엔드 코드를 변경해 주세요. + - [ ] https://panda-market-api.vercel.app의 API를 사용한 코드를 본인의 백엔드 API 코드로 변경하세요. + - [ ] (백엔드) 프론트엔드 코드에 맞게 백엔드 코드를 변경해 주세요. +- [ ] 백엔드 코드에 Swagger를 추가해 API 명세 문서를 생성해 주세요. + +--- + +### 백엔드 구현 요구사항 + +#### 상품 등록 +- [ ] "상품 등록하기" 버튼 클릭 시, 상품 정보 등록 API 엔드포인트에 요청을 보내 상품을 등록합니다. +- [ ] 상품 등록 시 필요한 필드(이름, 설명, 가격 등)의 유효성을 검증하는 미들웨어를 구현합니다. +- [ ] Multer 미들웨어를 사용하여 이미지 업로드 API를 구현해 주세요. + - [ ] 업로드된 이미지는 서버에 저장하고, 해당 이미지의 경로를 response 객체에 포함해 반환합니다. + +#### 상품 상세 +- [ ] 상품을 조회할 때, 해당 상품에 대한 댓글 리스트와 사용자가 '좋아요'를 눌렀는지 여부를 확인할 수 있도록 응답 객체에 포함시켜 반환해 주세요. + +#### 좋아요 기능 +- [ ] '좋아요' API를 만들어 주세요. + - [ ] 사용자는 상품 또는 게시글에 '좋아요'를 할 수 있습니다. + - [ ] $transaction을 사용해 주세요. +- [ ] '좋아요' 취소 API를 만들어 주세요. + - [ ] 사용자는 상품 또는 게시글에 '좋아요'를 취소할 수 있습니다. + - [ ] $transaction을 사용해 주세요. +- [ ] 상품 또는 게시글을 조회할 때, 사용자가 '좋아요'를 누른 항목인지 확인할 수 있도록 `isLiked` 필드를 응답 객체에 포함시켜 반환해 주세요. + +#### 에러 처리 +- [ ] 모든 예외 상황을 처리할 수 있는 에러 핸들러 미들웨어를 구현합니다. + - [ ] 서버 오류(500), 사용자 입력 오류(400 시리즈), 리소스 찾을 수 없음(404) 등 상황에 맞는 상태값을 반환합니다. + +#### 라우트 중복 제거 +- [ ] 중복되는 라우트 경로(예: `/users`에 대한 GET 및 POST 요청)를 `app.route()`로 통합해 중복을 제거합니다. +- [ ] `express.Router()`를 활용하여 중고마켓/자유게시판 관련 라우트를 별도의 모듈로 구분합니다. + +#### 인증 +- [ ] User 스키마를 작성해 주세요. + - [ ] `id`, `email`, `nickname`, `image`, `encryptedPassword`, `createdAt`, `updatedAt` 필드를 가집니다. +- [ ] 회원가입 API를 만들어 주세요. + - [ ] `email`, `nickname`, `password`를 입력하여 회원가입을 진행합니다. + - [ ] `password`는 해싱해 저장합니다. +- [ ] 로그인 API를 만들어 주세요. + - [ ] 사용자의 신원을 확인하고, 성공적인 인증 후에는 액세스 토큰을 발급해 response 객체에 포함해 반환합니다. + +--- + +### 기능별 인가 요구사항 + +#### 상품 기능 인가 +- [ ] 로그인한 사용자만 상품을 등록할 수 있습니다. +- [ ] 상품을 등록한 사용자만 해당 상품의 정보를 수정 및 삭제할 수 있습니다. +- [ ] 로그인한 사용자만 상품에 '좋아요'를 추가하거나 삭제할 수 있습니다. + +#### 게시글 기능 인가 +- [ ] 로그인한 사용자만 게시글을 등록할 수 있습니다. +- [ ] 게시글을 등록한 사용자만 해당 게시글 정보를 수정 및 삭제할 수 있습니다. +- [ ] 로그인한 사용자만 게시글에 '좋아요'를 추가하거나 삭제할 수 있습니다. + +#### 댓글 기능 인가 +- [ ] 로그인한 사용자만 상품에 댓글을 등록할 수 있습니다. +- [ ] 로그인한 사용자만 게시글에 댓글을 등록할 수 있습니다. +- [ ] 댓글을 등록한 사용자만 댓글을 수정하거나 삭제할 수 있습니다. + +--- + +### 심화 요구사항 + +#### 상태코드 (웹 API 관련) +- [ ] 프론트엔드에서는 서버 응답의 상태코드에 따라 적절한 사용자 피드백을 제공합니다. + +#### 인증 +- [ ] 토큰 기반 방식을 사용할 경우, 만료된 액세스 토큰을 새로 발급하는 리프레시 토큰 발급 기능을 구현합니다. (JWT Sliding Session 적용) + +#### OAuth를 활용한 인증 +- [ ] 구글 OAuth를 사용하여 회원가입 및 로그인 기능을 구현합니다. + +#### 프로젝트 구조 변경 +- [ ] 프로젝트의 구조와 복잡성을 관리하기 위해 MVC 패턴이나 Layered Architecture와 같은 설계 방식을 적용해 보세요. + +#### (생략 가능) 자유게시판 게시물 등록 +- [ ] 프론트엔드를 Next.js로 Migration 했을 경우에만 진행해 주세요. +- [ ] 게시물 등록 시 이미지 등록 기능을 구현합니다. + - [ ] 파일을 선택해 이미지를 업로드하고, Preview를 볼 수 있도록 구현합니다. + - [ ] 이미지는 최대 3개까지만 등록 가능하도록 구현해 주세요. +- [ ] 게시물 등록 시 필요한 필드(제목, 내용 등)의 유효성을 검증하는 미들웨어를 구현합니다. +- [ ] Multer 미들웨어를 사용하여 이미지 업로드 API를 구현해 주세요. + - [ ] 업로드된 이미지는 서버에 저장하고, 해당 이미지의 경로를 response 객체에 포함해 반환합니다. diff --git a/sprint10/http/articles.http b/sprint10/http/articles.http new file mode 100644 index 00000000..eed0b54e --- /dev/null +++ b/sprint10/http/articles.http @@ -0,0 +1,44 @@ +### +POST http://localhost:8000/articles +Content-Type: application/json + +{ + "title": "애플 맥북 얼마에 팔아요?", + "content": "2023년도 맥북 얼마에 파는 게 좋을까요?" +} + +### +GET http://localhost:8000/articles?limit=100 + +### +PATCH http://localhost:8000/articles/8f02ccce-2be2-4dfd-b8b9-4be79ff81b05 +Content-Type: application/json + +{ + "title": "올리브 오일은 언제 상하나요?", + "content": "올리브 오일은 올리브 기름을 말하는 것입니다. 하지만 올리브 오일이라고 부릅니다. 그것은 상하지만 상하지 않습니다. 결구 자연으로 돌아가 다른 올리브를 생성할 것이기 때문입니다." +} + +### +GET http://localhost:8000/articles?order=oldest&limit=3 + + +# 게시글 댓글 목록 조회 +### +GET http://localhost:8000/articles?keyword=증권 + +### +DELETE http://localhost:8000/articles/ + +# 게시글 댓글 목록 조회 +### +GET http://localhost:8000/articles/2cbe8fe7-6014-4c3a-b019-31f05be94a8f/comments + +# 게시글 댓글 등록 +### +POST http://localhost:8000/articles/2cbe8fe7-6014-4c3a-b019-31f05be94a8f/comments +Content-Type: application/json + +{ + "content": "매우 잘 자는 방법은 매우 힘들게 운동을 하고 자는 것이라고 생각하기 쉬운데 오히려 더 잠이 안 올수도 있다고 생각되지만 그래도 피곤하면 자게 돼 있죠.." +} diff --git a/sprint10/http/comment.http b/sprint10/http/comment.http new file mode 100644 index 00000000..5cb382b0 --- /dev/null +++ b/sprint10/http/comment.http @@ -0,0 +1,21 @@ +### +POST http://localhost:8000/articles/20e56878-61cb-443a-8d28-a11d8e251bbe/comments +Content-Type: application/json + +{ + "content": "부동산 투자는 시기보다는 지역과 매물이 중요하다고 볼 수 있습니다. 만약 강남에 부동산 투자를 할 수 있다면 당신은 시기를 따지지 마시고 바로 사시는 걸 추천합니다." +} + +### +PATCH http://localhost:8000/comments/ab25eb65-190a-46f6-9108-a8782b583f42 +Content-Type: application/json + +{ + "content": "인문고전 독서의 중요성에 대해서는 말해도 끝이 없을 거다. 매일 꾸준히 시간을 내서 의도적으로 매우 힘들게 가지 않으면 책을 읽을 수 없을 것이다. 힘써 최선을 다하여 책을 읽자!" +} + +### +DELETE http://localhost:8000/comments/3fb53aa1-caaf-4ec2-98d8-f097100d6737 + +### +GET http://localhost:8000/comments diff --git a/sprint10/http/products.http b/sprint10/http/products.http new file mode 100644 index 00000000..16a5c544 --- /dev/null +++ b/sprint10/http/products.http @@ -0,0 +1,38 @@ +### + +GET http://localhost:8000/products + +### + +GET http://localhost:8000/products?order=best&limit=3 + +### + +GET http://localhost:8000/products/7120aa7d-133e-45f0-9a0e-bfcb5f5bc53e + +### +POST http://localhost:8000/products +Content-Type: application/json + +{ + "name": "독서 필사의 중요성", + "description": "독서라는 매우 힘든 과정일 수 있습니다. 그렇기 때문에 그 내용이 한번에 이해되기는 쉽지 않기 때문에 노트북 필사를 통해 그 글을 쓰게 된 회로를 이해하는 것이 중요합니다.", + "price": 10055400, + "tags": ["필사", "독서", "노트북", "꾸준히"] +} + + +### +PATCH http://localhost:8000/products/d118116a-a9c7-47a3-96da-752e3105bfb8 +Content-Type: application/json + +{ + "name": "굽네치킨", + "description": "굽네 치킨은 튀기지 않아서 좋고 매우 맛있습니다. 특별히 남해마늘바사삭 치킨을 저는 좋아합니다.", + "price": 8000, + "tags": ["치킨", "굽네", "남해 마늘 바사삭"] +} + +### + +DELETE http://localhost:8000/products/7120aa7d-133e-45f0-9a0e-bfcb5f5bc53e \ No newline at end of file diff --git a/sprint10/orderByFunction.js b/sprint10/orderByFunction.js new file mode 100644 index 00000000..b4d73e2b --- /dev/null +++ b/sprint10/orderByFunction.js @@ -0,0 +1,12 @@ +const orderByFunction = (order) => { + switch (order) { + case 'recent': + return { createdAt: 'desc' }; + case 'best': + return { likes: 'desc' }; + default: + return { createdAt: 'desc' }; + } +} + +export default orderByFunction; \ No newline at end of file diff --git a/sprint10/package-lock.json b/sprint10/package-lock.json new file mode 100644 index 00000000..ed2f5398 --- /dev/null +++ b/sprint10/package-lock.json @@ -0,0 +1,1313 @@ +{ + "name": "sprint7", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "@faker-js/faker": "^9.2.0", + "@prisma/client": "^5.4.2", + "dotenv": "^16.3.1", + "express": "^4.18.2", + "is-email": "^1.0.2", + "is-uuid": "^1.0.2", + "prisma": "^5.4.2", + "superstruct": "^1.0.3" + }, + "devDependencies": { + "@types/express": "^5.0.0", + "@types/node": "^22.9.0", + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "nodemon": "^3.0.1" + } + }, + "node_modules/@faker-js/faker": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.2.0.tgz", + "integrity": "sha512-ulqQu4KMr1/sTFIYvqSdegHT8NIkt66tFAkugGnHA+1WAfEn6hMzNR+svjXGFRVLnapxvej67Z/LwchFrnLBUg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/fakerjs" + } + ], + "engines": { + "node": ">=18.0.0", + "npm": ">=9.0.0" + } + }, + "node_modules/@prisma/client": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.22.0.tgz", + "integrity": "sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==", + "hasInstallScript": true, + "engines": { + "node": ">=16.13" + }, + "peerDependencies": { + "prisma": "*" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + } + } + }, + "node_modules/@prisma/debug": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.22.0.tgz", + "integrity": "sha512-AUt44v3YJeggO2ZU5BkXI7M4hu9BF2zzH2iF2V5pyXT/lRTyWiElZ7It+bRH1EshoMRxHgpYg4VB6rCM+mG5jQ==" + }, + "node_modules/@prisma/engines": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.22.0.tgz", + "integrity": "sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA==", + "hasInstallScript": true, + "dependencies": { + "@prisma/debug": "5.22.0", + "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "@prisma/fetch-engine": "5.22.0", + "@prisma/get-platform": "5.22.0" + } + }, + "node_modules/@prisma/engines-version": { + "version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2.tgz", + "integrity": "sha512-2PTmxFR2yHW/eB3uqWtcgRcgAbG1rwG9ZriSvQw+nnb7c4uCr3RAcGMb6/zfE88SKlC1Nj2ziUvc96Z379mHgQ==" + }, + "node_modules/@prisma/fetch-engine": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.22.0.tgz", + "integrity": "sha512-bkrD/Mc2fSvkQBV5EpoFcZ87AvOgDxbG99488a5cexp5Ccny+UM6MAe/UFkUC0wLYD9+9befNOqGiIJhhq+HbA==", + "dependencies": { + "@prisma/debug": "5.22.0", + "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "@prisma/get-platform": "5.22.0" + } + }, + "node_modules/@prisma/get-platform": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.22.0.tgz", + "integrity": "sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q==", + "dependencies": { + "@prisma/debug": "5.22.0" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "dev": true, + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.0.tgz", + "integrity": "sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ==", + "dev": true, + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.1.tgz", + "integrity": "sha512-CRICJIl0N5cXDONAdlTv5ShATZ4HEwk6kDDIW2/w9qOWKg+NU/5F8wYRWCrONad0/UKkloNSmmyN/wX4rtpbVA==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "dev": true + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true + }, + "node_modules/@types/node": { + "version": "22.9.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.0.tgz", + "integrity": "sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ==", + "dev": true, + "dependencies": { + "undici-types": "~6.19.8" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.13", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz", + "integrity": "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==", + "dev": true + }, + "node_modules/@types/qs": { + "version": "6.9.17", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.17.tgz", + "integrity": "sha512-rX4/bPcfmvxHDv0XjfJELTTr+iB+tn032nPILqHm5wbthUUUuVtNGGqzhya9XUxjTP8Fpr0qYgSZZKxGY++svQ==", + "dev": true + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true + }, + "node_modules/@types/react": { + "version": "18.3.12", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.12.tgz", + "integrity": "sha512-D2wOSq/d6Agt28q7rSI3jhU7G6aiuzljDGZ2hTZHIkrTLUI+AF3WMeKkEZ9nN2fkBAlcktT6vcZjDFiIhMYEQw==", + "dev": true, + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dev": true, + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "dev": true, + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "dev": true + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", + "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.10", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-email": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-email/-/is-email-1.0.2.tgz", + "integrity": "sha512-UojUgD2EhDTBQ2SGKwrK9edce5phRzgLsP+V5+Uu2Swi+uvjVXgH3zduM3HhT9iaC/9Kq19/TYUbP0jPoi6ioA==" + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-uuid": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-uuid/-/is-uuid-1.0.2.tgz", + "integrity": "sha512-tCByphFcJgf2qmiMo5hMCgNAquNSagOetVetDvBXswGkNfoyEMvGH1yDlF8cbZbKnbVBr4Y5/rlpMz9umxyBkQ==" + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/nodemon": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.7.tgz", + "integrity": "sha512-hLj7fuMow6f0lbB0cD14Lz2xNjwsyruH251Pk4t/yIitCFJbmY1myuLlHm/q06aST4jg6EgAh74PIBBrRqpVAQ==", + "dev": true, + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/nodemon/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz", + "integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", + "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/prisma": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.22.0.tgz", + "integrity": "sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==", + "hasInstallScript": true, + "dependencies": { + "@prisma/engines": "5.22.0" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": ">=16.13" + }, + "optionalDependencies": { + "fsevents": "2.3.3" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/superstruct": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/superstruct/-/superstruct-1.0.4.tgz", + "integrity": "sha512-7JpaAoX2NGyoFlI9NBh66BQXGONc+uE+MRS5i2iOBKuS4e+ccgMDjATgZldkah+33DakBxDHiss9kvUcGAO8UQ==", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + } + } +} diff --git a/sprint10/package.json b/sprint10/package.json new file mode 100644 index 00000000..8e6ad369 --- /dev/null +++ b/sprint10/package.json @@ -0,0 +1,27 @@ +{ + "dependencies": { + "@faker-js/faker": "^9.2.0", + "@prisma/client": "^5.4.2", + "dotenv": "^16.3.1", + "express": "^4.18.2", + "is-email": "^1.0.2", + "is-uuid": "^1.0.2", + "prisma": "^5.4.2", + "superstruct": "^1.0.3" + }, + "devDependencies": { + "@types/express": "^5.0.0", + "@types/node": "^22.9.0", + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "nodemon": "^3.0.1" + }, + "type": "module", + "scripts": { + "dev": "nodemon app.js", + "start": "node app.js" + }, + "prisma": { + "seed": "node prisma/seed.js" + } +} diff --git a/sprint10/paginationHandler.js b/sprint10/paginationHandler.js new file mode 100644 index 00000000..be2e4cd5 --- /dev/null +++ b/sprint10/paginationHandler.js @@ -0,0 +1,30 @@ +import { PrismaClient } from '@prisma/client'; + +const prisma = new PrismaClient(); + +/** + * 파라미터로 startups, offset, limit 받아서 페이지네이션에 필요한 데이터를 추출하여 responseData 반환 +*/ +const productsPaginationHandler = async ( + product, offsetNum, limitNum) => { + + const totalProducts = await prisma.product.count(); + const currentPage = Math.floor(offsetNum / limitNum) + 1; + const totalPages = Math.ceil(totalProducts / limitNum); + const hasNextPage = offsetNum + limitNum < totalProducts; + const responseData = { totalProducts, currentPage, product, totalPages, hasNextPage }; + return responseData; +} + +const articlesPaginationHandler = async ( + article, offsetNum, limitNum) => { + + const totalArticles = await prisma.article.count(); + const currentPage = Math.floor(offsetNum / limitNum) + 1; + const totalPages = Math.ceil(totalArticles / limitNum); + const hasNextPage = offsetNum + limitNum < totalArticles; + const responseData = { totalArticles, currentPage, article, totalPages, hasNextPage }; + return responseData; +} + +export {productsPaginationHandler, articlesPaginationHandler}; \ No newline at end of file diff --git a/sprint10/prisma/migrations/20241116061602_add_product_email/migration.sql b/sprint10/prisma/migrations/20241116061602_add_product_email/migration.sql new file mode 100644 index 00000000..fd3e4092 --- /dev/null +++ b/sprint10/prisma/migrations/20241116061602_add_product_email/migration.sql @@ -0,0 +1,31 @@ +-- CreateTable +CREATE TABLE "Product" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "email" TEXT NOT NULL, + "description" TEXT NOT NULL, + "price" INTEGER NOT NULL, + "tags" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Product_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Article" ( + "id" TEXT NOT NULL, + "title" TEXT NOT NULL, + "content" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "productId" TEXT NOT NULL, + + CONSTRAINT "Article_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "Product_email_key" ON "Product"("email"); + +-- AddForeignKey +ALTER TABLE "Article" ADD CONSTRAINT "Article_productId_fkey" FOREIGN KEY ("productId") REFERENCES "Product"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/sprint10/prisma/migrations/20241116094525_remove_product_email/migration.sql b/sprint10/prisma/migrations/20241116094525_remove_product_email/migration.sql new file mode 100644 index 00000000..589a1eb4 --- /dev/null +++ b/sprint10/prisma/migrations/20241116094525_remove_product_email/migration.sql @@ -0,0 +1,11 @@ +/* + Warnings: + + - You are about to drop the column `email` on the `Product` table. All the data in the column will be lost. + +*/ +-- DropIndex +DROP INDEX "Product_email_key"; + +-- AlterTable +ALTER TABLE "Product" DROP COLUMN "email"; diff --git a/sprint10/prisma/migrations/20241116130153_add_product_likes/migration.sql b/sprint10/prisma/migrations/20241116130153_add_product_likes/migration.sql new file mode 100644 index 00000000..fba5f43e --- /dev/null +++ b/sprint10/prisma/migrations/20241116130153_add_product_likes/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Product" ADD COLUMN "likes" INTEGER; diff --git a/sprint10/prisma/migrations/20241116131817_add_product_likes/migration.sql b/sprint10/prisma/migrations/20241116131817_add_product_likes/migration.sql new file mode 100644 index 00000000..6a9e1402 --- /dev/null +++ b/sprint10/prisma/migrations/20241116131817_add_product_likes/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - Made the column `likes` on table `Product` required. This step will fail if there are existing NULL values in that column. + +*/ +-- AlterTable +ALTER TABLE "Product" ALTER COLUMN "likes" SET NOT NULL; diff --git a/sprint10/prisma/migrations/20241116155152_add_product_likes_opptional/migration.sql b/sprint10/prisma/migrations/20241116155152_add_product_likes_opptional/migration.sql new file mode 100644 index 00000000..4c23414d --- /dev/null +++ b/sprint10/prisma/migrations/20241116155152_add_product_likes_opptional/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable +ALTER TABLE "Product" ALTER COLUMN "likes" DROP NOT NULL, +ALTER COLUMN "likes" SET DEFAULT 0; diff --git a/sprint10/prisma/migrations/20241116174703_add_comment_model/migration.sql b/sprint10/prisma/migrations/20241116174703_add_comment_model/migration.sql new file mode 100644 index 00000000..53e67eb6 --- /dev/null +++ b/sprint10/prisma/migrations/20241116174703_add_comment_model/migration.sql @@ -0,0 +1,34 @@ +/* + Warnings: + + - You are about to drop the column `productId` on the `Article` table. All the data in the column will be lost. + - The `tags` column on the `Product` table would be dropped and recreated. This will lead to data loss if there is data in the column. + +*/ +-- DropForeignKey +ALTER TABLE "Article" DROP CONSTRAINT "Article_productId_fkey"; + +-- AlterTable +ALTER TABLE "Article" DROP COLUMN "productId"; + +-- AlterTable +ALTER TABLE "Product" DROP COLUMN "tags", +ADD COLUMN "tags" TEXT[]; + +-- CreateTable +CREATE TABLE "Comment" ( + "id" TEXT NOT NULL, + "content" TEXT NOT NULL, + "articleId" TEXT NOT NULL, + "productId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Comment_pkey" PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "Comment" ADD CONSTRAINT "Comment_articleId_fkey" FOREIGN KEY ("articleId") REFERENCES "Article"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Comment" ADD CONSTRAINT "Comment_productId_fkey" FOREIGN KEY ("productId") REFERENCES "Product"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/sprint10/prisma/migrations/20241117113216_add_aritcle_likes/migration.sql b/sprint10/prisma/migrations/20241117113216_add_aritcle_likes/migration.sql new file mode 100644 index 00000000..4a906a0c --- /dev/null +++ b/sprint10/prisma/migrations/20241117113216_add_aritcle_likes/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Article" ADD COLUMN "likes" INTEGER DEFAULT 0; diff --git a/sprint10/prisma/migrations/20241117144233_add_comment_product_opptional/migration.sql b/sprint10/prisma/migrations/20241117144233_add_comment_product_opptional/migration.sql new file mode 100644 index 00000000..0a954413 --- /dev/null +++ b/sprint10/prisma/migrations/20241117144233_add_comment_product_opptional/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Comment" ALTER COLUMN "productId" DROP NOT NULL; diff --git a/sprint10/prisma/migrations/20241117145605_add_comment_likes/migration.sql b/sprint10/prisma/migrations/20241117145605_add_comment_likes/migration.sql new file mode 100644 index 00000000..a2b57ac1 --- /dev/null +++ b/sprint10/prisma/migrations/20241117145605_add_comment_likes/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Comment" ADD COLUMN "likes" INTEGER DEFAULT 0; diff --git a/sprint10/prisma/migrations/migration_lock.toml b/sprint10/prisma/migrations/migration_lock.toml new file mode 100644 index 00000000..fbffa92c --- /dev/null +++ b/sprint10/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "postgresql" \ No newline at end of file diff --git a/sprint10/prisma/mocks/articlesMock.js b/sprint10/prisma/mocks/articlesMock.js new file mode 100644 index 00000000..aa6d26eb --- /dev/null +++ b/sprint10/prisma/mocks/articlesMock.js @@ -0,0 +1,127 @@ +export const ARTICLES = [ + { + title: "애플 맥북 얼마에 팔아요?", + content: "2023년도 맥북 얼마에 파는 게 좋을까요?", + likes: 12 + }, + { + title: "갤럭시 안드로이드 폰의 가격은?", + content: "최신 갤럭시 안드로이드 폰의 가격은 얼마일까요?", + likes: 20 + }, + { + title: "부동산 투자의 시기는 언제인가요?", + content: "현재 부동산 투자의 적절한 시기는 언제일까요?", + likes: 5 + }, + { + title: "여러분은 어떻게 증권 투자를 하세요?", + content: "어떤 증권 투자 방법이 효과적이라고 생각하시나요?", + likes: 15 + }, + { + title: "토스증권의 장단점은?", + content: "토스증권의 장단점은 무엇이라 생각하시나요?", + likes: 8 + }, + { + title: "중국에서 가장 인기 있는 전자제품은?", + content: "중국에서 가장 인기 있는 전자제품은 무엇일까요?", + likes: 10 + }, + { + title: "건강식품을 추천해 주세요.", + content: "현재 가장 인기 있는 건강식품은 무엇일까요?", + likes: 25 + }, + { + title: "은행 대출 금리의 변화는?", + content: "은행 대출 금리의 변화는 어떻게 될까요?", + likes: 3 + }, + { + title: "여행을 준비하세요.", + content: "여행을 준비할 때 필요한 준비물은?", + likes: 12 + }, + { + title: "여러분은 어떻게 운동을 하세요?", + content: "현재 가장 인기 있는 운동 방법은?", + likes: 18 + }, + { + title: "기대되는 yeni 게임은?", + content: "기대되는 yeni 게임은 무엇일까요?", + likes: 20 + }, + { + title: "가장 최근에 본 영화는?", + content: "현재 가장 인기 있는 영화는?", + likes: 15 + }, + { + title: "여러분은 음악을 어떻게 감상하시나요?", + content: "현재 가장 인기 있는 음악 감상 방법은?", + likes: 8 + }, + { + title: "프로그래밍 언어 추천", + content: "기초 프로그래밍 언어는 무엇일까요?", + likes: 10 + }, + { + title: "디아블로 4의 출시일은?", + content: "다음으로 기대되는 게임은?", + likes: 25 + }, + { + title: "여러분은 어떻게 잇몸을 관리하세요?", + content: "현재 가장 중요하게 생각하는 건강 관리는?", + likes: 5 + }, + { + title: "여러분은 어떻게 하루를 시작하시나요?", + content: "효과적인 하루 시작 방법은?", + likes: 12 + }, + { + title: "여러분의 인생 목표는 무엇인가요?", + content: "현재의 목표는?", + likes: 18 + }, + { + title: "여러분은 كيف 어떻게 침대에서 자르세요?", + content: "최근 잠을 이루는 방법은?", + likes: 10 + }, + { + title: "여러분은 어떻게 체중 감량을 하나요?", + content: "체중 감량 방법은?", + likes: 15 + }, + { + title: "여러분은 어떻게 방을 청소 하나요?", + content: "효과적인 방 청소 방법은?", + likes: 8 + }, + { + title: "여러분은 어떻게 나의 옷장 관리 하나요?", + content: "옷장 관리 방법은?", + likes: 12 + }, + { + title: "여러분은 어떻게 일상생활을 합리화 하나요?", + content: "일상생활을 어떻게 합리화 하나요?", + likes: 20 + }, + { + title: "여러분은 어떻게 일과를 계획 하나요?", + content: "일과를 계획하는 방법은?", + likes: 10 + }, + { + title: "여러분은 어떻게 자기 계발을 하나요?", + content: "자기 계발 방법은?", + likes: 25 + } +]; diff --git a/sprint10/prisma/mocks/commentsMock.js b/sprint10/prisma/mocks/commentsMock.js new file mode 100644 index 00000000..19bf0fef --- /dev/null +++ b/sprint10/prisma/mocks/commentsMock.js @@ -0,0 +1,127 @@ +export const COMMENTS = [ + { + content: "스트레스를 받을 때 어떻게 관리하시나요?", + likes: 13, + articleId: "b538d0ac-561f-4854-8934-61518facbe89" + }, + { + content: "음악 감상이 취미인데, 새로운 음악 추천 좀 해주세요.", + likes: 19, + articleId: "1f94f843-9ab3-471a-8d1f-3500599c93d5" + }, + { + content: "집에서 할 수 있는 유익한 활동 뭐가 있을까요?", + likes: 15, + articleId: "138ad878-d7f5-4a14-a877-584ab9269db3" + }, + { + content: "여러분은 어떤 방식으로 힐링하시나요?", + likes: 16, + articleId: "e06a4d8f-2102-48f1-8241-6187c9542a30" + }, + { + content: "새로운 취미를 시작하고 싶은데, 추천 좀 해주세요.", + likes: 13, + articleId: "92f18b7a-7bf2-4cf6-a366-628027196fba" + }, + { + content: "여러분은 어떻게 자기 관리하시나요?", + likes: 18, + articleId: "cf6297c4-5e63-4d6c-a29e-2e0de6507047" + }, + { + content: "인터넷 쇼핑할 때 꿀팁 있으면 공유해주세요.", + likes: 12, + articleId: "20e56878-61cb-443a-8d28-a11d8e251bbe" + }, + { + content: "여행 사진을 예쁘게 찍는 방법 좀 알려주세요.", + likes: 20, + articleId: "3d37910b-2421-4c8b-94e5-c8358cdec8d8" + }, + { + content: "스트레칭을 꾸준히 하시나요? 그 이유가 궁금해요.", + likes: 15, + articleId: "55b5cefb-ea57-4ac1-bfdf-ada0fae5504d" + }, + { + content: "다이어트 중인데, 추천할 만한 간식 있나요?", + likes: 14, + articleId: "5b41acc9-32a8-4080-97d2-94e4220241ae" + }, + { + content: "새로운 언어를 배우고 싶은데, 어떻게 시작해야 할까요?", + likes: 13, + articleId: "673b9cf3-daf0-4972-9e5d-d3bc2d4b0a5f" + }, + { + content: "장시간 앉아 있을 때 피로를 푸는 방법이 궁금해요.", + likes: 17, + articleId: "ad02013b-b27a-411a-85b6-5a7d4a44b848" + }, + { + content: "여행을 자주 하시나요? 가장 기억에 남는 여행지는 어디인가요?", + likes: 16, + articleId: "a5f1e7d8-0338-4cf2-a60e-395fee95bab6" + }, + { + content: "반려동물과의 생활에서 주의해야 할 점이 있나요?", + likes: 14, + articleId: "0cdbb48b-2d03-48a1-ad5d-84c2f0561023" + }, + { + content: "좋아하는 음식을 추천해 주세요.", + likes: 18, + articleId: "10d2c858-c1bf-4933-8289-1c3f4f078c42" + }, + { + content: "여러분의 최애 영화는 무엇인가요?", + likes: 20, + articleId: "d9ea8238-04d3-48e6-bfab-8c2ddd5dfd97" + }, + { + content: "여러분의 일상 속 작은 행복은 무엇인가요?", + likes: 12, + articleId: "67b0ccb9-03ef-4336-a314-73f08e4bf26d" + }, + { + content: "좋은 수면 습관을 기르기 위한 팁이 있나요?", + likes: 11, + articleId: "413b8c84-52db-4a9f-a0e0-10c6c76576f3" + }, + { + content: "자기계발을 위해 추천하는 강연이나 책이 있나요?", + likes: 17, + articleId: "2cbe8fe7-6014-4c3a-b019-31f05be94a8f" + }, + { + content: "주말을 잘 보내는 방법에 대해 이야기해 주세요.", + likes: 19, + articleId: "12a66d3d-a02a-4457-83df-6df72d03eeef" + }, + { + content: "여러분이 가장 좋아하는 계절은 무엇인가요?", + likes: 13, + articleId: "4aa01ba9-5329-4722-ae9d-4bf6728d3f38" + }, + { + content: "스트레스를 받을 때 어떻게 관리하시나요?(복제된 데이터)", + likes: 15, + articleId: "39ac8df8-4b7f-4147-9420-bc55488d7b37" + }, + { + content: "여러분의 일상 속 작은 행복은 무엇인가요?(복제된 데이터)", + likes: 18, + articleId: "9eba2cef-cd31-4fc2-b0b8-63c082cb09c1" + }, + { + content: "좋은 수면 습관을 기르기 위한 팁이 있나요?(복제된 데이터)", + likes: 16, + articleId: "48486088-d275-4935-8fb0-302c60fedd62" + }, + { + content: "여러분이 가장 좋아하는 영화는 무엇인가요?(복제된 데이터)", + likes: 14, + articleId: "3b9a1811-5648-4ed3-a075-8c96b55957e9" + } +]; diff --git a/sprint10/prisma/mocks/productsMock.js b/sprint10/prisma/mocks/productsMock.js new file mode 100644 index 00000000..b71c4563 --- /dev/null +++ b/sprint10/prisma/mocks/productsMock.js @@ -0,0 +1,240 @@ +export const PRODUCTS = [ + { + name: 'iPhone 13', + description: 'Latest Apple smartphone with A15 Bionic chip.', + price: 999.99, + likes: 23, + tags: ['smartphone', 'apple', 'ios'] + }, + { + name: 'Samsung Galaxy S21', + description: 'Flagship Samsung smartphone with Exynos processor.', + price: 899.99, + likes: 12, + tags: ['smartphone', 'samsung', 'android'] + }, + { + name: 'Dell XPS 13', + description: 'High-performance ultrabook with Intel i7 processor.', + price: 1299.99, + likes: 45, + tags: ['laptop', 'dell', 'windows'] + }, + { + name: 'Sony WH-1000XM4', + description: 'Noise-cancelling wireless headphones.', + price: 349.99, + likes: 67, + tags: ['headphones', 'wireless', 'sony'] + }, + { + name: 'Apple Watch Series 6', + description: 'Smartwatch with health tracking features.', + price: 399.99, + likes: 11, + tags: ['smartwatch', 'apple', 'ios'] + }, + { + name: 'Amazon Echo Dot', + description: 'Smart speaker with Alexa voice assistant.', + price: 49.99, + likes: 38, + tags: ['smart speaker', 'amazon', 'alexa'] + }, + { + name: 'GoPro Hero 9', + description: 'Action camera with 5K video recording.', + price: 449.99, + likes: 28, + tags: ['action camera', 'gopro', 'adventure'] + }, + { + name: 'Nintendo Switch', + description: 'Portable gaming console with versatile gameplay options.', + price: 299.99, + likes: 49, + tags: ['gaming console', 'nintendo', 'portable'] + }, + { + name: 'Canon EOS R5', + description: 'Full-frame mirrorless camera with 8K video recording.', + price: 3899.99, + likes: 17, + tags: ['camera', 'canon', 'mirrorless'] + }, + { + name: 'Dyson V11', + description: 'Cordless vacuum cleaner with powerful suction.', + price: 599.99, + likes: 25, + tags: ['vacuum cleaner', 'dyson', 'cordless'] + }, + { + name: 'Bose SoundLink Revolve', + description: 'Portable Bluetooth speaker with 360-degree sound.', + price: 199.99, + likes: 41, + tags: ['speaker', 'bose', 'portable'] + }, + { + name: 'Apple iPad Pro', + description: 'High-performance tablet with Apple Pencil support.', + price: 1099.99, + likes: 31, + tags: ['tablet', 'apple', 'ios'] + }, + { + name: 'Fitbit Charge 4', + description: 'Fitness tracker with heart rate monitoring.', + price: 149.99, + likes: 19, + tags: ['fitness tracker', 'fitbit', 'health'] + }, + { + name: 'Roku Streaming Stick+', + description: 'Streaming device with 4K HDR support.', + price: 49.99, + likes: 13, + tags: ['streaming device', 'roku', '4k'] + }, + { + name: 'Logitech MX Master 3', + description: 'Wireless mouse with ergonomic design.', + price: 99.99, + likes: 26, + tags: ['mouse', 'logitech', 'wireless'] + }, + { + name: 'HP Spectre x360', + description: 'Convertible laptop with touchscreen display.', + price: 1399.99, + likes: 44, + tags: ['laptop', 'hp', 'convertible'] + }, + { + name: 'Kindle Paperwhite', + description: 'E-reader with high-resolution display.', + price: 129.99, + likes: 35, + tags: ['e-reader', 'amazon', 'reading'] + }, + { + name: 'Garmin Fenix 6', + description: 'Multisport GPS smartwatch.', + price: 649.99, + likes: 22, + tags: ['smartwatch', 'garmin', 'gps'] + }, + { + name: 'Samsung QLED TV', + description: '65-inch 4K UHD Smart TV.', + price: 1599.99, + likes: 51, + tags: ['smart TV', 'samsung', 'qled'] + }, + { + name: 'DJI Mavic Air 2', + description: 'Compact drone with 4K video recording.', + price: 799.99, + likes: 18, + tags: ['drone', 'dji', 'aerial'] + }, + { + name: 'Philips Hue Starter Kit', + description: 'Smart lighting system with three bulbs and a bridge.', + price: 179.99, + likes: 29, + tags: ['smart lighting', 'philips', 'hue'] + }, + { + name: 'Nest Learning Thermostat', + description: 'Smart thermostat with energy-saving features.', + price: 249.99, + likes: 15, + tags: ['thermostat', 'nest', 'smart home'] + }, + { + name: 'Microsoft Surface Pro 7', + description: '2-in-1 laptop with detachable keyboard.', + price: 899.99, + likes: 42, + tags: ['laptop', 'microsoft', 'surface'] + }, + { + name: 'Sonos One', + description: 'Smart speaker with voice control.', + price: 199.99, + likes: 36, + tags: ['speaker', 'sonos', 'smart speaker'] + }, + { + name: 'Anker PowerCore 10000', + description: 'Portable charger with high capacity.', + price: 29.99, + likes: 20, + tags: ['portable charger', 'anker', 'power bank'] + }, + { + name: 'Ring Video Doorbell', + description: 'Smart doorbell with video recording.', + price: 99.99, + likes: 16, + tags: ['doorbell', 'ring', 'smart doorbell'] + }, + { + name: 'Sony PlayStation 5', + description: 'Next-gen gaming console with advanced graphics.', + price: 499.99, + likes: 50, + tags: ['gaming console', 'sony', 'playstation'] + }, + { + name: 'ASUS ROG Zephyrus G14', + description: 'Gaming laptop with powerful graphics card.', + price: 1449.99, + likes: 48, + tags: ['laptop', 'asus', 'gaming laptop'] + }, + { + name: 'Oculus Quest 2', + description: 'Standalone VR headset with immersive experiences.', + price: 299.99, + likes: 32, + tags: ['VR headset', 'oculus', 'virtual reality'] + }, + { + name: 'Instant Pot Duo', + description: 'Multi-use pressure cooker.', + price: 89.99, + likes: 24, + tags: ['pressure cooker', 'instant pot', 'cooking'] + }, + { + name: 'Apple AirPods Pro', + description: 'Wireless earbuds with noise cancellation.', + price: 249.99, + likes: 39, + tags: ['wireless earbuds', 'apple', 'airpods'] + }, + { + name: 'Beats by Dre Solo3', + description: 'On-ear headphones with high-quality sound.', + price: 199.99, + likes: 46, + tags: ['headphones', 'beats', 'dre'] + }, + { + name: 'Tile Pro', + description: 'Bluetooth tracker for finding lost items.', + price: 34.99, + likes: 21, + tags: ['tracker', 'tile', 'bluetooth'] + }, + { + name: 'KitchenAid Stand Mixer', + description: 'Stand mixer with various attachments.', + price: 379.99, + likes: 14, + tags: ['kitchen appliance', 'kitchenaid', 'mixer'] + } +]; diff --git a/sprint10/prisma/schema.prisma b/sprint10/prisma/schema.prisma new file mode 100644 index 00000000..ebf1e5e9 --- /dev/null +++ b/sprint10/prisma/schema.prisma @@ -0,0 +1,48 @@ +// This is your Prisma schema file, +// learn more about it in the docs: https://pris.ly/d/prisma-schema + +// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions? +// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init + +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +model Product { + id String @id @default(uuid()) + name String + description String + price Int + likes Int? @default(0) // likes 필드에 기본값을 0으로 지정 + tags String[] + ProductComment Comment[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model Article { + id String @id @default(uuid()) + title String + content String + likes Int? @default(0) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + ArticleComment Comment[] +} + +model Comment { + id String @id @default(uuid()) + content String + likes Int? @default(0) + article Article @relation(fields: [articleId], references: [id], onDelete: Cascade) + articleId String + product Product? @relation(fields: [productId], references: [id], onDelete: Cascade) + productId String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} diff --git a/sprint10/prisma/seed.js b/sprint10/prisma/seed.js new file mode 100644 index 00000000..653d149d --- /dev/null +++ b/sprint10/prisma/seed.js @@ -0,0 +1,61 @@ +import { PrismaClient } from "@prisma/client"; +import { PRODUCTS } from "./mocks/productsMock.js"; +import { ARTICLES } from "./mocks/articlesMock.js"; +import { COMMENTS } from "./mocks/commentsMock.js"; +// import { faker } from '@faker-js/faker'; + +const prisma = new PrismaClient(); + +async function main() { + +// const TEST_DATA = []; + +// for (let i = 0; i < 10; i++) { +// TEST_DATA.push({ +// name: faker.commerce.productName(), +// avatar: faker.image.avatar(), +// description: faker.commerce.productDescription(), +// price: faker.commerce.price(), +// tags: faker.commerce.productAdjective(), + +// _id: faker.string.uuid(), +// avatar: faker.image.avatar(), +// birthday: faker.date.birthdate(), +// email: faker.internet.email(), +// firstName: faker.person.firstName(), +// lastName: faker.person.lastName(), +// sex: faker.person.sexType(), +// subscriptionTier: faker.helpers.arrayElement(['free', 'basic', 'business']), +// }); +// } +// console.log(TEST_DATA); + + // // 기존 데이터 삭제 + // await prisma.product.deleteMany(); + // await prisma.article.deleteMany(); + await prisma.comment.deleteMany(); // article(id) 데이터 삭제되면 comment 데이터 못 씀(유지하기). + // 목 데이터 삽입 + // await prisma.product.createMany({ + // data: PRODUCTS, + // skipDuplicates: true, + // }); + // await prisma.article.createMany({ + // data: ARTICLES, + // skipDuplicates: true, + // }); + await prisma.comment.createMany({ + data: COMMENTS, + skipDuplicates: true, + }); +} + +main() +.then(async () => { + await prisma.$disconnect(); +}) +.catch(async (e) => { + console.error(e); + await prisma.$disconnect(); + process.exit(1); +}); +console.log("success"); diff --git a/sprint10/structs.js b/sprint10/structs.js new file mode 100644 index 00000000..2fd6a313 --- /dev/null +++ b/sprint10/structs.js @@ -0,0 +1,30 @@ +import * as s from 'superstruct'; + +/** Products Routes **/ + +export const CreateProduct = s.object({ + name: s.size(s.string(), 1, 10), + description: s.size(s.string(), 10, 100), + price: s.number(), + tags: s.array(s.string()), +}); + +export const PatchProduct = s.partial(CreateProduct); + +/** Articles Routes **/ + +export const CreateArticle = s.object({ + title: s.size(s.string(), 1, Infinity), + content: s.size(s.string(), 1, Infinity), +}); + +export const PatchArticle = s.partial(CreateArticle); + + +/** Comments Routes **/ + +export const CreateComment = s.object({ + content: s.size(s.string(), 1, Infinity), +}); + +export const PatchComment = s.partial(CreateComment); \ No newline at end of file