From fb81c77722ea596589be804235762ae4c374b364 Mon Sep 17 00:00:00 2001 From: aowjarkwk Date: Tue, 21 May 2024 20:14:34 +0900 Subject: [PATCH 01/21] =?UTF-8?q?seed=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 134 +++++ app.js | 24 + data/mock.js | 22 + data/seed.js | 11 + models/product.js | 47 ++ package-lock.json | 1262 +++++++++++++++++++++++++++++++++++++++++++++ package.json | 15 + 7 files changed, 1515 insertions(+) create mode 100644 .gitignore create mode 100644 app.js create mode 100644 data/mock.js create mode 100644 data/seed.js create mode 100644 models/product.js create mode 100644 package-lock.json create mode 100644 package.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..989cf15 --- /dev/null +++ b/.gitignore @@ -0,0 +1,134 @@ +# 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 +.cache + +# 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.* + +# env +.env +env.js \ No newline at end of file diff --git a/app.js b/app.js new file mode 100644 index 0000000..004f1cc --- /dev/null +++ b/app.js @@ -0,0 +1,24 @@ +import express from "express"; +import mongoose from "mongoose"; +import { DATABASE_URL } from "./env.js"; + +mongoose.connect(DATABASE_URL).then(() => console.log("Connected to DB")); +const app = express(); + +app.use(express.json()); + +const asyncHandler = (handler) => { + return async (req, res) => { + try { + await handler(req, res); + } catch (e) { + if (e.name === "ValidationError") { + res.status(400).send({ message: e.message }); + } else if (e.name === "CastError") { + res.status(404).send({ message: "존재하지 않는 상품입니다." }); + } else { + res.status(500).send({ message: "서버 에러입니다." }); + } + } + }; +}; diff --git a/data/mock.js b/data/mock.js new file mode 100644 index 0000000..f56e1c7 --- /dev/null +++ b/data/mock.js @@ -0,0 +1,22 @@ +const data = [ + { + favoriteCount: 0, + ownerId: 1, + images: ["https://example.com/..."], + tags: ["판다인형", "인형", "판다"], + price: 700000, + description: "판다인형 판다", + name: "판다인형", + }, + { + favoriteCount: 2, + ownerId: 2, + images: ["https://example.com/..."], + tags: ["판다인형", "인형", "판다"], + price: 7000, + description: "판다인형 안판다", + name: "판다인형 안파는 판다", + }, +]; + +export default data; diff --git a/data/seed.js b/data/seed.js new file mode 100644 index 0000000..89ec145 --- /dev/null +++ b/data/seed.js @@ -0,0 +1,11 @@ +import mongoose from "mongoose"; +import { DATABASE_URL } from "../env.js"; +import Product from "../models/product.js"; +import data from "./mock.js"; + +mongoose.connect(DATABASE_URL); + +await Product.deleteMany({}); +await Product.insertMany(data); + +mongoose.connection.close(); diff --git a/models/product.js b/models/product.js new file mode 100644 index 0000000..318abcc --- /dev/null +++ b/models/product.js @@ -0,0 +1,47 @@ +import mongoose from "mongoose"; + +const ProductSchema = new mongoose.Schema( + { + favoriteCount: { + type: Number, + default: 0, + }, + ownerId: { + type: Number, + required: true, + }, + + images: { + type: [String], + required: true, + }, + tags: { + type: [String], + required: true, + }, + price: { + type: Number, + required: true, + default: 0, + }, + description: { + type: String, + }, + name: { + type: String, + required: true, + }, + isFavorite: { + type: Boolean, + required: true, + default: false, + }, + }, + { + timestamps: true, + } +); + +const Product = mongoose.model("Product", ProductSchema); + +export default Product; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..e76448a --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1262 @@ +{ + "name": "QA-sprint-mission", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "express": "^4.19.2", + "mongoose": "^8.4.0" + }, + "devDependencies": { + "nodemon": "^3.1.0" + } + }, + "node_modules/@mongodb-js/saslprep": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.7.tgz", + "integrity": "sha512-dCHW/oEX0KJ4NjDULBo3JiOaK5+6axtpBbS+ao2ZInoAL9/YRQLhXzSNAFz7hP4nzLkIqsfYAK/PDE3+XHny0Q==", + "dependencies": { + "sparse-bitfield": "^3.0.3" + } + }, + "node_modules/@types/webidl-conversions": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==" + }, + "node_modules/@types/whatwg-url": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", + "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==", + "dependencies": { + "@types/webidl-conversions": "*" + } + }, + "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.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", + "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "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.11.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/body-parser/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/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "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/bson": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/bson/-/bson-6.7.0.tgz", + "integrity": "sha512-w2IquM5mYzYZv6rs3uN2DZTOBe2a0zXLj53TGDqwF4l6Sz/XsISrisXOJihArF9+BZ6Cq/GjVht7Sjfmri7ytQ==", + "engines": { + "node": ">=16.20.1" + } + }, + "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.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "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/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/debug/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "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/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": "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/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.19.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", + "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.2", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.6.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "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/express/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/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "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.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "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/finalhandler/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/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "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==", + "dev": true, + "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-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/kareem": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz", + "integrity": "sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==", + "engines": { + "node": ">=12.0.0" + } + }, + "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/memory-pager": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==" + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + }, + "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/mongodb": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.6.2.tgz", + "integrity": "sha512-ZF9Ugo2JCG/GfR7DEb4ypfyJJyiKbg5qBYKRintebj8+DNS33CyGMkWbrS9lara+u+h+yEOGSRiLhFO/g1s1aw==", + "dependencies": { + "@mongodb-js/saslprep": "^1.1.5", + "bson": "^6.7.0", + "mongodb-connection-string-url": "^3.0.0" + }, + "engines": { + "node": ">=16.20.1" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.188.0", + "@mongodb-js/zstd": "^1.1.0", + "gcp-metadata": "^5.2.0", + "kerberos": "^2.0.1", + "mongodb-client-encryption": ">=6.0.0 <7", + "snappy": "^7.2.2", + "socks": "^2.7.1" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "gcp-metadata": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + }, + "socks": { + "optional": true + } + } + }, + "node_modules/mongodb-connection-string-url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.1.tgz", + "integrity": "sha512-XqMGwRX0Lgn05TDB4PyG2h2kKO/FfWJyCzYQbIhXUxz7ETt0I/FqHjUeqj37irJ+Dl1ZtU82uYyj14u2XsZKfg==", + "dependencies": { + "@types/whatwg-url": "^11.0.2", + "whatwg-url": "^13.0.0" + } + }, + "node_modules/mongoose": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.4.0.tgz", + "integrity": "sha512-fgqRMwVEP1qgRYfh+tUe2YBBFnPO35FIg2lfFH+w9IhRGg1/ataWGIqvf/MjwM29cZ60D5vSnqtN2b8Qp0sOZA==", + "dependencies": { + "bson": "^6.7.0", + "kareem": "2.6.3", + "mongodb": "6.6.2", + "mpath": "0.9.0", + "mquery": "5.0.0", + "ms": "2.1.3", + "sift": "17.1.3" + }, + "engines": { + "node": ">=16.20.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mongoose" + } + }, + "node_modules/mpath": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz", + "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mquery": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/mquery/-/mquery-5.0.0.tgz", + "integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==", + "dependencies": { + "debug": "4.x" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "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/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.0", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.0.tgz", + "integrity": "sha512-xqlktYlDMCepBJd43ZQhjWwMw2obW/JRvkrLxq5RCNcuDDX1DbcPT+qT1IlIIdf+DhnWs90JpTMe+Y5KxOchvA==", + "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/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.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "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.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + }, + "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/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/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "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.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "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/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/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.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/sift": { + "version": "17.1.3", + "resolved": "https://registry.npmjs.org/sift/-/sift-17.1.3.tgz", + "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==" + }, + "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/sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "dependencies": { + "memory-pager": "^1.0.2" + } + }, + "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/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/tr46": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", + "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==", + "dependencies": { + "punycode": "^2.3.0" + }, + "engines": { + "node": ">=14" + } + }, + "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/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" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-13.0.0.tgz", + "integrity": "sha512-9WWbymnqj57+XEuqADHrCJ2eSXzn8WXIW/YSGaZtb2WKAInQ6CHfaUUcTyyver0p8BDg5StLQq8h1vtZuwmOig==", + "dependencies": { + "tr46": "^4.1.1", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=16" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..e91d2bd --- /dev/null +++ b/package.json @@ -0,0 +1,15 @@ +{ + "dependencies": { + "express": "^4.19.2", + "mongoose": "^8.4.0" + }, + "devDependencies": { + "nodemon": "^3.1.0" + }, + "type": "module", + "scripts": { + "dev": "nodemon app.js", + "start": "node app.js", + "seed": "node data/seed.js" + } +} From de043b5b69f71b5099232f521578479e0cfa9912 Mon Sep 17 00:00:00 2001 From: aowjarkwk Date: Tue, 21 May 2024 20:35:41 +0900 Subject: [PATCH 02/21] =?UTF-8?q?=EC=83=81=ED=92=88=20=EB=93=B1=EB=A1=9D,?= =?UTF-8?q?=20=EC=83=81=ED=92=88=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.js | 34 ++++++++++++++++++++++++++++++++++ data/mock.js | 6 ++++-- requests.http | 20 ++++++++++++++++++++ 3 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 requests.http diff --git a/app.js b/app.js index 004f1cc..ee6035a 100644 --- a/app.js +++ b/app.js @@ -1,6 +1,7 @@ import express from "express"; import mongoose from "mongoose"; import { DATABASE_URL } from "./env.js"; +import Product from "./models/product.js"; mongoose.connect(DATABASE_URL).then(() => console.log("Connected to DB")); const app = express(); @@ -22,3 +23,36 @@ const asyncHandler = (handler) => { } }; }; + +app.get( + "/products", + asyncHandler(async (req, res) => { + /** + * 쿼리 파라미터 + * - page : 페이지 번호 + * - pageSize : 페이지 당 상품 수 + * - orderBy : 정렬 기준 favorite, recent (기본값: recent) + * - keyword : 검색 키워드 + */ + const orderBy = req.query.sort; + const count = Number(req.query.count) || 0; + + const sortOption = orderBy === "favorite" ? { favoriteCount: "desc" } : { createdAt: "desc" }; + + const product = await Product.find().sort(sortOption).limit(count); + + res.send(product); + }) +); + +app.post( + "/products", + asyncHandler(async (req, res) => { + const newProduct = await Product.create(req.body); + res.status(201).send(newProduct); + }) +); + +app.listen(3000, () => { + console.log("Server is running on port 3000"); +}); diff --git a/data/mock.js b/data/mock.js index f56e1c7..be18f37 100644 --- a/data/mock.js +++ b/data/mock.js @@ -2,7 +2,7 @@ const data = [ { favoriteCount: 0, ownerId: 1, - images: ["https://example.com/..."], + images: ["https://sitem.ssgcdn.com/62/11/49/item/1000559491162_i1_1100.jpg"], tags: ["판다인형", "인형", "판다"], price: 700000, description: "판다인형 판다", @@ -11,7 +11,9 @@ const data = [ { favoriteCount: 2, ownerId: 2, - images: ["https://example.com/..."], + images: [ + "https://view01.wemep.co.kr/wmp-product/4/879/2515748794/pm_ebifv5nrjsyf.jpg?1683280710&f=webp&w=460&h=460", + ], tags: ["판다인형", "인형", "판다"], price: 7000, description: "판다인형 안판다", diff --git a/requests.http b/requests.http new file mode 100644 index 0000000..abccb34 --- /dev/null +++ b/requests.http @@ -0,0 +1,20 @@ +GET http://localhost:3000/products + +### + +GET http://localhost:3000/products + +### + +POST http://localhost:3000/products +Content-Type: application/json + +{ + "name": "판다랑 불곰 교환원해요", + "description": "세종시청에서 교환원합니다.", + "price": 20000, + "tags": ["판다", "불곰"], + "images": ["https://www.wishbucket.io/_next/image?url=https%3A%2F%2Fd2gfz7wkiigkmv.cloudfront.net%2Fpickin%2F2%2F1%2F2%2FHereyhSJRMOmUw7I5uWAxg&w=640&q=75","https://wimg.mk.co.kr/meet/2021/09/image_listtop_2021_854860_1630738087.jpg"], + "ownerId":1 + +} From 2f75548c7dc4f95caf60ecc2c11dce188ac0ae45 Mon Sep 17 00:00:00 2001 From: aowjarkwk Date: Wed, 22 May 2024 09:17:00 +0900 Subject: [PATCH 03/21] =?UTF-8?q?=EC=8A=A4=ED=82=A4=EB=A7=88=EC=9D=98=20?= =?UTF-8?q?=ED=95=84=EB=93=9C=20=EC=88=9C=EC=84=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- models/product.js | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/models/product.js b/models/product.js index 318abcc..dae54d8 100644 --- a/models/product.js +++ b/models/product.js @@ -2,40 +2,39 @@ import mongoose from "mongoose"; const ProductSchema = new mongoose.Schema( { - favoriteCount: { - type: Number, - default: 0, + name: { + type: String, + required: true, }, - ownerId: { + description: { + type: String, + }, + price: { type: Number, required: true, + default: 0, }, - - images: { + tags: { type: [String], required: true, }, - tags: { + images: { type: [String], required: true, }, - price: { + favoriteCount: { type: Number, - required: true, default: 0, }, - description: { - type: String, - }, - name: { - type: String, - required: true, - }, isFavorite: { type: Boolean, required: true, default: false, }, + ownerId: { + type: Number, + required: true, + }, }, { timestamps: true, From 75f72023e200d5c42a27c1f3f13bc5ed537ecb49 Mon Sep 17 00:00:00 2001 From: aowjarkwk Date: Wed, 22 May 2024 09:40:16 +0900 Subject: [PATCH 04/21] =?UTF-8?q?=EC=83=81=ED=92=88=20=EC=83=81=EC=84=B8?= =?UTF-8?q?=20=EC=A1=B0=ED=9A=8C,=20=EC=83=81=ED=92=88=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 상품 목록 조회 페이지네이션 추가 --- app.js | 70 +++++++++++++++++++++++++++++++++++++++++++++++---- requests.http | 22 +++++++++++++++- 2 files changed, 86 insertions(+), 6 deletions(-) diff --git a/app.js b/app.js index ee6035a..ea246db 100644 --- a/app.js +++ b/app.js @@ -24,27 +24,59 @@ const asyncHandler = (handler) => { }; }; +//상품 목록 조회 app.get( "/products", asyncHandler(async (req, res) => { /** * 쿼리 파라미터 - * - page : 페이지 번호 - * - pageSize : 페이지 당 상품 수 + * - offset : 가져올 데이터의 시작 지점 + * - limit : 한 번에 가져올 데이터의 개수 * - orderBy : 정렬 기준 favorite, recent (기본값: recent) * - keyword : 검색 키워드 */ - const orderBy = req.query.sort; - const count = Number(req.query.count) || 0; + const offset = Number(req.query.offset) || 0; + const limit = Number(req.query.limit) || 10; + const orderBy = req.query.orderBy; + const keyword = req.query.keyword || ""; const sortOption = orderBy === "favorite" ? { favoriteCount: "desc" } : { createdAt: "desc" }; - const product = await Product.find().sort(sortOption).limit(count); + // 제목과 내용에서 키워드를 검색하는 쿼리 + const query = keyword + ? { + $or: [{ name: { $regex: keyword, $options: "i" } }, { description: { $regex: keyword, $options: "i" } }], + } + : {}; + + const products = await Product.find(query) + .select("_id name price images createdAt favoriteCount isFavorite") + .sort(sortOption) + .skip(offset) + .limit(limit); + + const totalProducts = await Product.countDocuments(query); + + res.send({ + products, + totalProducts, + currentOffset: offset, + limit: limit, + }); + }) +); + +//상품 상세 조회 +app.get( + "/products/:id", + asyncHandler(async (req, res) => { + const product = await Product.findById(req.params.id).select("-updatedAt"); res.send(product); }) ); +// 상품 등록 app.post( "/products", asyncHandler(async (req, res) => { @@ -53,6 +85,34 @@ app.post( }) ); +// 상품 수정 +app.patch( + "/products/:id", + asyncHandler(async (req, res) => { + const product = await Product.findById(req.params.id); + + if (!product) { + res.status(404).send({ message: "존재하지 않는 상품입니다." }); + return; + } + const disallowedFields = { + favoriteCount: true, + isFavorite: true, + ownerId: true, + }; + + Object.keys(req.body).forEach((key) => { + if (!disallowedFields[key]) { + product[key] = req.body[key]; + } + }); + + await product.save(); + + res.send(product); + }) +); + app.listen(3000, () => { console.log("Server is running on port 3000"); }); diff --git a/requests.http b/requests.http index abccb34..41fb525 100644 --- a/requests.http +++ b/requests.http @@ -2,7 +2,15 @@ GET http://localhost:3000/products ### -GET http://localhost:3000/products +GET http://localhost:3000/products?offset=0&limit=0&keyword=불곰 + +### + +GET http://localhost:3000/products?keyword=판다 + +### + +GET http://localhost:3000/products/664d393f71f073e051e9aaca ### @@ -16,5 +24,17 @@ Content-Type: application/json "tags": ["판다", "불곰"], "images": ["https://www.wishbucket.io/_next/image?url=https%3A%2F%2Fd2gfz7wkiigkmv.cloudfront.net%2Fpickin%2F2%2F1%2F2%2FHereyhSJRMOmUw7I5uWAxg&w=640&q=75","https://wimg.mk.co.kr/meet/2021/09/image_listtop_2021_854860_1630738087.jpg"], "ownerId":1 +} + +### +PATCH http://localhost:3000/products/664d393f71f073e051e9aaca +Content-Type: application/json + +{ + "name":"판다 안팔려서 안판다", + "description":"안판다고 했지만 사실은 판다", + "price":7000, + "tags":["판다","안판다"] } + From 7f34ae2c03f65f6e5792c45a3650967dba6b4671 Mon Sep 17 00:00:00 2001 From: aowjarkwk Date: Wed, 22 May 2024 09:52:42 +0900 Subject: [PATCH 05/21] =?UTF-8?q?=EC=83=81=ED=92=88=20=EC=82=AD=EC=A0=9C,?= =?UTF-8?q?=20=EC=A2=8B=EC=95=84=EC=9A=94,=20=EC=A2=8B=EC=95=84=EC=9A=94?= =?UTF-8?q?=20=EC=B7=A8=EC=86=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.js | 71 +++++++++++++++++++++++++++++++++++++++++++++++++-- requests.http | 12 +++++++++ 2 files changed, 81 insertions(+), 2 deletions(-) diff --git a/app.js b/app.js index ea246db..0a00cb0 100644 --- a/app.js +++ b/app.js @@ -24,7 +24,7 @@ const asyncHandler = (handler) => { }; }; -//상품 목록 조회 +// 상품 목록 조회 app.get( "/products", asyncHandler(async (req, res) => { @@ -66,7 +66,7 @@ app.get( }) ); -//상품 상세 조회 +// 상품 상세 조회 app.get( "/products/:id", asyncHandler(async (req, res) => { @@ -113,6 +113,73 @@ app.patch( }) ); +// 상품 삭제 +app.delete( + "/products/:id", + asyncHandler(async (req, res) => { + const product = await Product.findByIdAndDelete(req.params.id); + + if (!product) { + res.status(404).send({ message: "존재하지 않는 상품입니다." }); + return; + } + + res.sendStatus(204); + }) +); + +// 상품 좋아요 +app.patch( + "/products/:id/like", + asyncHandler(async (req, res) => { + const product = await Product.findById(req.params.id); + + if (!product) { + res.status(404).send({ message: "존재하지 않는 상품입니다." }); + return; + } + + if (product.isFavorite) { + res.status(400).send({ message: "이미 좋아요 처리된 상품입니다." }); + return; + } + + product.favoriteCount += 1; + product.isFavorite = true; + + await product.save(); + + res.send(product); + }) +); + +// 상품 좋아요 취소 +app.patch( + "/products/:id/unlike", + asyncHandler(async (req, res) => { + const product = await Product.findById(req.params.id); + + if (!product) { + res.status(404).send({ message: "존재하지 않는 상품입니다." }); + return; + } + + if (!product.isFavorite) { + res.status(400).send({ message: "아직 좋아요 처리되지 않은 상품입니다." }); + return; + } + + if (product.favoriteCount > 0) { + product.favoriteCount -= 1; + } + product.isFavorite = false; + + await product.save(); + + res.send(product); + }) +); + app.listen(3000, () => { console.log("Server is running on port 3000"); }); diff --git a/requests.http b/requests.http index 41fb525..c2aae31 100644 --- a/requests.http +++ b/requests.http @@ -38,3 +38,15 @@ Content-Type: application/json "tags":["판다","안판다"] } + +### + +DELETE http://localhost:3000/products/664d393f71f073e051e9aaca + +### +PATCH http://localhost:3000/products/664d3eb1c4d678fe7647c427/like + +### + +PATCH http://localhost:3000/products/664d3eb1c4d678fe7647c427/unlike + From d5dbbdfad93b8cd7fd57bc3113382c74a12998fd Mon Sep 17 00:00:00 2001 From: aowjarkwk Date: Wed, 22 May 2024 09:57:44 +0900 Subject: [PATCH 06/21] =?UTF-8?q?=ED=99=98=EA=B2=BD=20=EB=B3=80=EC=88=98?= =?UTF-8?q?=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.js | 10 +++++----- data/seed.js | 5 +++-- package-lock.json | 12 ++++++++++++ package.json | 1 + 4 files changed, 21 insertions(+), 7 deletions(-) diff --git a/app.js b/app.js index 0a00cb0..6b0a406 100644 --- a/app.js +++ b/app.js @@ -1,9 +1,11 @@ import express from "express"; import mongoose from "mongoose"; -import { DATABASE_URL } from "./env.js"; import Product from "./models/product.js"; -mongoose.connect(DATABASE_URL).then(() => console.log("Connected to DB")); +import * as dotenv from "dotenv"; + +dotenv.config(); +mongoose.connect(process.env.DATABASE_URL).then(() => console.log("Connected to DB")); const app = express(); app.use(express.json()); @@ -180,6 +182,4 @@ app.patch( }) ); -app.listen(3000, () => { - console.log("Server is running on port 3000"); -}); +app.listen(process.env.PORT || 3000, () => console.log("Server Started")); diff --git a/data/seed.js b/data/seed.js index 89ec145..b2fd350 100644 --- a/data/seed.js +++ b/data/seed.js @@ -1,9 +1,10 @@ +import * as dotenv from "dotenv"; import mongoose from "mongoose"; -import { DATABASE_URL } from "../env.js"; import Product from "../models/product.js"; import data from "./mock.js"; -mongoose.connect(DATABASE_URL); +dotenv.config(); +mongoose.connect(process.env.DATABASE_URL); await Product.deleteMany({}); await Product.insertMany(data); diff --git a/package-lock.json b/package-lock.json index e76448a..d6df146 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,6 +5,7 @@ "packages": { "": { "dependencies": { + "dotenv": "^16.4.5", "express": "^4.19.2", "mongoose": "^8.4.0" }, @@ -289,6 +290,17 @@ "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", diff --git a/package.json b/package.json index e91d2bd..1dec6dd 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,6 @@ { "dependencies": { + "dotenv": "^16.4.5", "express": "^4.19.2", "mongoose": "^8.4.0" }, From b26cab1fe6f998b9b522d6f52c4d76030c858e46 Mon Sep 17 00:00:00 2001 From: aowjarkwk Date: Wed, 22 May 2024 10:23:00 +0900 Subject: [PATCH 07/21] =?UTF-8?q?cors=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.js | 15 +++++++++++---- package-lock.json | 21 +++++++++++++++++++++ package.json | 1 + 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/app.js b/app.js index 6b0a406..723b901 100644 --- a/app.js +++ b/app.js @@ -1,13 +1,17 @@ +import cors from "cors"; +import * as dotenv from "dotenv"; import express from "express"; import mongoose from "mongoose"; import Product from "./models/product.js"; -import * as dotenv from "dotenv"; - dotenv.config(); mongoose.connect(process.env.DATABASE_URL).then(() => console.log("Connected to DB")); const app = express(); - +app.use(cors()); +const corsOptions = { + origin: ["http://127.0.0.1:3000", "https://panda-market.com"], +}; +app.use(cors(corsOptions)); app.use(express.json()); const asyncHandler = (handler) => { @@ -44,7 +48,6 @@ app.get( const sortOption = orderBy === "favorite" ? { favoriteCount: "desc" } : { createdAt: "desc" }; - // 제목과 내용에서 키워드를 검색하는 쿼리 const query = keyword ? { $or: [{ name: { $regex: keyword, $options: "i" } }, { description: { $regex: keyword, $options: "i" } }], @@ -73,6 +76,10 @@ app.get( "/products/:id", asyncHandler(async (req, res) => { const product = await Product.findById(req.params.id).select("-updatedAt"); + if (!product) { + res.status(404).send({ message: "존재하지 않는 상품입니다." }); + return; + } res.send(product); }) diff --git a/package-lock.json b/package-lock.json index d6df146..0f2104d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,6 +5,7 @@ "packages": { "": { "dependencies": { + "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.19.2", "mongoose": "^8.4.0" @@ -236,6 +237,18 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -881,6 +894,14 @@ "node": ">=0.10.0" } }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-inspect": { "version": "1.13.1", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", diff --git a/package.json b/package.json index 1dec6dd..03de25d 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,6 @@ { "dependencies": { + "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.19.2", "mongoose": "^8.4.0" From 3d86d6978b60db3359672063b29f8420516dea10 Mon Sep 17 00:00:00 2001 From: aowjarkwk Date: Wed, 22 May 2024 15:30:33 +0900 Subject: [PATCH 08/21] =?UTF-8?q?Mongo=20DB=EB=A5=BC=20PostgreSQL=EB=A1=9C?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD=20-=20=EC=83=81=ED=92=88=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20-=20=EC=83=81=ED=92=88=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=20=EC=A1=B0=ED=9A=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.js | 57 +--- http/products.http | 52 +++ package-lock.json | 299 ++++++------------ package.json | 16 +- .../20240522062619_init/migration.sql | 16 + prisma/migrations/migration_lock.toml | 3 + prisma/schema.prisma | 28 ++ 7 files changed, 212 insertions(+), 259 deletions(-) create mode 100644 http/products.http create mode 100644 prisma/migrations/20240522062619_init/migration.sql create mode 100644 prisma/migrations/migration_lock.toml create mode 100644 prisma/schema.prisma diff --git a/app.js b/app.js index 723b901..ebff140 100644 --- a/app.js +++ b/app.js @@ -1,17 +1,9 @@ -import cors from "cors"; -import * as dotenv from "dotenv"; +import { PrismaClient } from "@prisma/client"; import express from "express"; -import mongoose from "mongoose"; -import Product from "./models/product.js"; +const prisma = new PrismaClient(); -dotenv.config(); -mongoose.connect(process.env.DATABASE_URL).then(() => console.log("Connected to DB")); const app = express(); -app.use(cors()); -const corsOptions = { - origin: ["http://127.0.0.1:3000", "https://panda-market.com"], -}; -app.use(cors(corsOptions)); + app.use(express.json()); const asyncHandler = (handler) => { @@ -34,40 +26,8 @@ const asyncHandler = (handler) => { app.get( "/products", asyncHandler(async (req, res) => { - /** - * 쿼리 파라미터 - * - offset : 가져올 데이터의 시작 지점 - * - limit : 한 번에 가져올 데이터의 개수 - * - orderBy : 정렬 기준 favorite, recent (기본값: recent) - * - keyword : 검색 키워드 - */ - const offset = Number(req.query.offset) || 0; - const limit = Number(req.query.limit) || 10; - const orderBy = req.query.orderBy; - const keyword = req.query.keyword || ""; - - const sortOption = orderBy === "favorite" ? { favoriteCount: "desc" } : { createdAt: "desc" }; - - const query = keyword - ? { - $or: [{ name: { $regex: keyword, $options: "i" } }, { description: { $regex: keyword, $options: "i" } }], - } - : {}; - - const products = await Product.find(query) - .select("_id name price images createdAt favoriteCount isFavorite") - .sort(sortOption) - .skip(offset) - .limit(limit); - - const totalProducts = await Product.countDocuments(query); - - res.send({ - products, - totalProducts, - currentOffset: offset, - limit: limit, - }); + const products = await prisma.product.findMany(); + res.send(products); }) ); @@ -75,7 +35,12 @@ app.get( app.get( "/products/:id", asyncHandler(async (req, res) => { - const product = await Product.findById(req.params.id).select("-updatedAt"); + const { id } = req.params; + const product = await prisma.product.findUnique({ + where: { + id, + }, + }); if (!product) { res.status(404).send({ message: "존재하지 않는 상품입니다." }); return; diff --git a/http/products.http b/http/products.http new file mode 100644 index 0000000..2914f0c --- /dev/null +++ b/http/products.http @@ -0,0 +1,52 @@ +GET http://localhost:3000/products + +### + +GET http://localhost:3000/products?offset=0&limit=0&keyword=불곰 + +### + +GET http://localhost:3000/products?keyword=판다 + +### + +GET http://localhost:3000/products/8edc68a8-8361-4411-bb9f-dcb1a4c3d0de + +### + +POST http://localhost:3000/products +Content-Type: application/json + +{ + "name": "판다랑 불곰 교환원해요", + "description": "세종시청에서 교환원합니다.", + "price": 20000, + "tags": ["판다", "불곰"], + "images": ["https://www.wishbucket.io/_next/image?url=https%3A%2F%2Fd2gfz7wkiigkmv.cloudfront.net%2Fpickin%2F2%2F1%2F2%2FHereyhSJRMOmUw7I5uWAxg&w=640&q=75","https://wimg.mk.co.kr/meet/2021/09/image_listtop_2021_854860_1630738087.jpg"], + "ownerId":1 +} + +### + +PATCH http://localhost:3000/products/664d393f71f073e051e9aaca +Content-Type: application/json + +{ + "name":"판다 안팔려서 안판다", + "description":"안판다고 했지만 사실은 판다", + "price":7000, + "tags":["판다","안판다"] +} + + +### + +DELETE http://localhost:3000/products/664d393f71f073e051e9aaca + +### +PATCH http://localhost:3000/products/664d3eb1c4d678fe7647c427/like + +### + +PATCH http://localhost:3000/products/664d3eb1c4d678fe7647c427/unlike + diff --git a/package-lock.json b/package-lock.json index 0f2104d..2db532a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,34 +5,73 @@ "packages": { "": { "dependencies": { - "cors": "^2.8.5", - "dotenv": "^16.4.5", - "express": "^4.19.2", - "mongoose": "^8.4.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": { - "nodemon": "^3.1.0" + "nodemon": "^3.0.1" } }, - "node_modules/@mongodb-js/saslprep": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.7.tgz", - "integrity": "sha512-dCHW/oEX0KJ4NjDULBo3JiOaK5+6axtpBbS+ao2ZInoAL9/YRQLhXzSNAFz7hP4nzLkIqsfYAK/PDE3+XHny0Q==", + "node_modules/@prisma/client": { + "version": "5.14.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.14.0.tgz", + "integrity": "sha512-akMSuyvLKeoU4LeyBAUdThP/uhVP3GuLygFE3MlYzaCb3/J8SfsYBE5PkaFuLuVpLyA6sFoW+16z/aPhNAESqg==", + "hasInstallScript": true, + "engines": { + "node": ">=16.13" + }, + "peerDependencies": { + "prisma": "*" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + } + } + }, + "node_modules/@prisma/debug": { + "version": "5.14.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.14.0.tgz", + "integrity": "sha512-iq56qBZuFfX3fCxoxT8gBX33lQzomBU0qIUaEj1RebsKVz1ob/BVH1XSBwwwvRVtZEV1b7Fxx2eVu34Ge/mg3w==" + }, + "node_modules/@prisma/engines": { + "version": "5.14.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.14.0.tgz", + "integrity": "sha512-lgxkKZ6IEygVcw6IZZUlPIfLQ9hjSYAtHjZ5r64sCLDgVzsPFCi2XBBJgzPMkOQ5RHzUD4E/dVdpn9+ez8tk1A==", + "hasInstallScript": true, "dependencies": { - "sparse-bitfield": "^3.0.3" + "@prisma/debug": "5.14.0", + "@prisma/engines-version": "5.14.0-25.e9771e62de70f79a5e1c604a2d7c8e2a0a874b48", + "@prisma/fetch-engine": "5.14.0", + "@prisma/get-platform": "5.14.0" } }, - "node_modules/@types/webidl-conversions": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", - "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==" + "node_modules/@prisma/engines-version": { + "version": "5.14.0-25.e9771e62de70f79a5e1c604a2d7c8e2a0a874b48", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.14.0-25.e9771e62de70f79a5e1c604a2d7c8e2a0a874b48.tgz", + "integrity": "sha512-ip6pNkRo1UxWv+6toxNcYvItNYaqQjXdFNGJ+Nuk2eYtRoEdoF13wxo7/jsClJFFenMPVNVqXQDV0oveXnR1cA==" + }, + "node_modules/@prisma/fetch-engine": { + "version": "5.14.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.14.0.tgz", + "integrity": "sha512-VrheA9y9DMURK5vu8OJoOgQpxOhas3qF0IBHJ8G/0X44k82kc8E0w98HCn2nhnbOOMwbWsJWXfLC2/F8n5u0gQ==", + "dependencies": { + "@prisma/debug": "5.14.0", + "@prisma/engines-version": "5.14.0-25.e9771e62de70f79a5e1c604a2d7c8e2a0a874b48", + "@prisma/get-platform": "5.14.0" + } }, - "node_modules/@types/whatwg-url": { - "version": "11.0.5", - "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", - "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==", + "node_modules/@prisma/get-platform": { + "version": "5.14.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.14.0.tgz", + "integrity": "sha512-/yAyBvcEjRv41ynZrhdrPtHgk47xLRRq/o5eWGcUpBJ1YrUZTYB8EoPiopnP7iQrMATK8stXQdPOoVlrzuTQZw==", "dependencies": { - "@types/webidl-conversions": "*" + "@prisma/debug": "5.14.0" } }, "node_modules/accepts": { @@ -141,14 +180,6 @@ "node": ">=8" } }, - "node_modules/bson": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/bson/-/bson-6.7.0.tgz", - "integrity": "sha512-w2IquM5mYzYZv6rs3uN2DZTOBe2a0zXLj53TGDqwF4l6Sz/XsISrisXOJihArF9+BZ6Cq/GjVht7Sjfmri7ytQ==", - "engines": { - "node": ">=16.20.1" - } - }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -237,22 +268,11 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, - "node_modules/cors": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", - "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", - "dependencies": { - "object-assign": "^4", - "vary": "^1" - }, - "engines": { - "node": ">= 0.10" - } - }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, "dependencies": { "ms": "2.1.2" }, @@ -268,7 +288,8 @@ "node_modules/debug/node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true }, "node_modules/define-data-property": { "version": "1.1.4", @@ -644,6 +665,11 @@ "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", @@ -674,13 +700,10 @@ "node": ">=0.12.0" } }, - "node_modules/kareem": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz", - "integrity": "sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==", - "engines": { - "node": ">=12.0.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", @@ -690,11 +713,6 @@ "node": ">= 0.6" } }, - "node_modules/memory-pager": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", - "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==" - }, "node_modules/merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", @@ -750,100 +768,6 @@ "node": "*" } }, - "node_modules/mongodb": { - "version": "6.6.2", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.6.2.tgz", - "integrity": "sha512-ZF9Ugo2JCG/GfR7DEb4ypfyJJyiKbg5qBYKRintebj8+DNS33CyGMkWbrS9lara+u+h+yEOGSRiLhFO/g1s1aw==", - "dependencies": { - "@mongodb-js/saslprep": "^1.1.5", - "bson": "^6.7.0", - "mongodb-connection-string-url": "^3.0.0" - }, - "engines": { - "node": ">=16.20.1" - }, - "peerDependencies": { - "@aws-sdk/credential-providers": "^3.188.0", - "@mongodb-js/zstd": "^1.1.0", - "gcp-metadata": "^5.2.0", - "kerberos": "^2.0.1", - "mongodb-client-encryption": ">=6.0.0 <7", - "snappy": "^7.2.2", - "socks": "^2.7.1" - }, - "peerDependenciesMeta": { - "@aws-sdk/credential-providers": { - "optional": true - }, - "@mongodb-js/zstd": { - "optional": true - }, - "gcp-metadata": { - "optional": true - }, - "kerberos": { - "optional": true - }, - "mongodb-client-encryption": { - "optional": true - }, - "snappy": { - "optional": true - }, - "socks": { - "optional": true - } - } - }, - "node_modules/mongodb-connection-string-url": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.1.tgz", - "integrity": "sha512-XqMGwRX0Lgn05TDB4PyG2h2kKO/FfWJyCzYQbIhXUxz7ETt0I/FqHjUeqj37irJ+Dl1ZtU82uYyj14u2XsZKfg==", - "dependencies": { - "@types/whatwg-url": "^11.0.2", - "whatwg-url": "^13.0.0" - } - }, - "node_modules/mongoose": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.4.0.tgz", - "integrity": "sha512-fgqRMwVEP1qgRYfh+tUe2YBBFnPO35FIg2lfFH+w9IhRGg1/ataWGIqvf/MjwM29cZ60D5vSnqtN2b8Qp0sOZA==", - "dependencies": { - "bson": "^6.7.0", - "kareem": "2.6.3", - "mongodb": "6.6.2", - "mpath": "0.9.0", - "mquery": "5.0.0", - "ms": "2.1.3", - "sift": "17.1.3" - }, - "engines": { - "node": ">=16.20.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mongoose" - } - }, - "node_modules/mpath": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz", - "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/mquery": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/mquery/-/mquery-5.0.0.tgz", - "integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==", - "dependencies": { - "debug": "4.x" - }, - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -894,14 +818,6 @@ "node": ">=0.10.0" } }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/object-inspect": { "version": "1.13.1", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", @@ -946,6 +862,21 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/prisma": { + "version": "5.14.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.14.0.tgz", + "integrity": "sha512-gCNZco7y5XtjrnQYeDJTiVZmT/ncqCr5RY1/Cf8X2wgLRmyh9ayPAGBNziI4qEE4S6SxCH5omQLVo9lmURaJ/Q==", + "hasInstallScript": true, + "dependencies": { + "@prisma/engines": "5.14.0" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": ">=16.13" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -964,14 +895,6 @@ "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", "dev": true }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "engines": { - "node": ">=6" - } - }, "node_modules/qs": { "version": "6.11.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", @@ -1144,11 +1067,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/sift": { - "version": "17.1.3", - "resolved": "https://registry.npmjs.org/sift/-/sift-17.1.3.tgz", - "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==" - }, "node_modules/simple-update-notifier": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", @@ -1161,14 +1079,6 @@ "node": ">=10" } }, - "node_modules/sparse-bitfield": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", - "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", - "dependencies": { - "memory-pager": "^1.0.2" - } - }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -1177,6 +1087,14 @@ "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", @@ -1218,17 +1136,6 @@ "nodetouch": "bin/nodetouch.js" } }, - "node_modules/tr46": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", - "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==", - "dependencies": { - "punycode": "^2.3.0" - }, - "engines": { - "node": ">=14" - } - }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -1270,26 +1177,6 @@ "engines": { "node": ">= 0.8" } - }, - "node_modules/webidl-conversions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", - "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", - "engines": { - "node": ">=12" - } - }, - "node_modules/whatwg-url": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-13.0.0.tgz", - "integrity": "sha512-9WWbymnqj57+XEuqADHrCJ2eSXzn8WXIW/YSGaZtb2WKAInQ6CHfaUUcTyyver0p8BDg5StLQq8h1vtZuwmOig==", - "dependencies": { - "tr46": "^4.1.1", - "webidl-conversions": "^7.0.0" - }, - "engines": { - "node": ">=16" - } } } } diff --git a/package.json b/package.json index 03de25d..7d7a89b 100644 --- a/package.json +++ b/package.json @@ -1,17 +1,19 @@ { "dependencies": { - "cors": "^2.8.5", - "dotenv": "^16.4.5", - "express": "^4.19.2", - "mongoose": "^8.4.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": { - "nodemon": "^3.1.0" + "nodemon": "^3.0.1" }, "type": "module", "scripts": { "dev": "nodemon app.js", - "start": "node app.js", - "seed": "node data/seed.js" + "start": "node app.js" } } diff --git a/prisma/migrations/20240522062619_init/migration.sql b/prisma/migrations/20240522062619_init/migration.sql new file mode 100644 index 0000000..6a86190 --- /dev/null +++ b/prisma/migrations/20240522062619_init/migration.sql @@ -0,0 +1,16 @@ +-- CreateTable +CREATE TABLE "Product" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "description" TEXT NOT NULL, + "price" DOUBLE PRECISION NOT NULL, + "tags" TEXT[], + "images" TEXT[], + "favoriteCount" INTEGER NOT NULL DEFAULT 0, + "isFavorite" BOOLEAN NOT NULL DEFAULT false, + "ownerId" INTEGER NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Product_pkey" PRIMARY KEY ("id") +); diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..fbffa92 --- /dev/null +++ b/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/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..03d286e --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,28 @@ +// 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 Float + tags String[] + images String[] + favoriteCount Int @default(0) + isFavorite Boolean @default(false) + ownerId Int + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} From 77a4ec5782e2713807c82b9b8be51b8f55fa5064 Mon Sep 17 00:00:00 2001 From: aowjarkwk Date: Wed, 22 May 2024 15:36:10 +0900 Subject: [PATCH 09/21] =?UTF-8?q?=EC=83=81=ED=92=88=20=EB=93=B1=EB=A1=9D,?= =?UTF-8?q?=20=EC=83=81=ED=92=88=20=EC=88=98=EC=A0=95,=20=EC=83=81?= =?UTF-8?q?=ED=92=88=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.js | 35 +++++++++++++++++------------------ http/products.http | 4 ++-- 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/app.js b/app.js index ebff140..7bfa0d3 100644 --- a/app.js +++ b/app.js @@ -54,8 +54,10 @@ app.get( app.post( "/products", asyncHandler(async (req, res) => { - const newProduct = await Product.create(req.body); - res.status(201).send(newProduct); + const product = await prisma.product.create({ + data: req.body, + }); + res.status(201).send(product); }) ); @@ -63,25 +65,17 @@ app.post( app.patch( "/products/:id", asyncHandler(async (req, res) => { - const product = await Product.findById(req.params.id); - + const { id } = req.params; + const product = await prisma.product.update({ + where: { + id, + }, + data: req.body, + }); if (!product) { res.status(404).send({ message: "존재하지 않는 상품입니다." }); return; } - const disallowedFields = { - favoriteCount: true, - isFavorite: true, - ownerId: true, - }; - - Object.keys(req.body).forEach((key) => { - if (!disallowedFields[key]) { - product[key] = req.body[key]; - } - }); - - await product.save(); res.send(product); }) @@ -91,7 +85,12 @@ app.patch( app.delete( "/products/:id", asyncHandler(async (req, res) => { - const product = await Product.findByIdAndDelete(req.params.id); + const { id } = req.params; + const product = await prisma.product.delete({ + where: { + id, + }, + }); if (!product) { res.status(404).send({ message: "존재하지 않는 상품입니다." }); diff --git a/http/products.http b/http/products.http index 2914f0c..c56456d 100644 --- a/http/products.http +++ b/http/products.http @@ -28,7 +28,7 @@ Content-Type: application/json ### -PATCH http://localhost:3000/products/664d393f71f073e051e9aaca +PATCH http://localhost:3000/products/8edc68a8-8361-4411-bb9f-dcb1a4c3d0de Content-Type: application/json { @@ -41,7 +41,7 @@ Content-Type: application/json ### -DELETE http://localhost:3000/products/664d393f71f073e051e9aaca +DELETE http://localhost:3000/products/8edc68a8-8361-4411-bb9f-dcb1a4c3d0de ### PATCH http://localhost:3000/products/664d3eb1c4d678fe7647c427/like From 175c432b2878eddc59bea371d32a1b2275be0a2f Mon Sep 17 00:00:00 2001 From: aowjarkwk Date: Wed, 22 May 2024 15:46:27 +0900 Subject: [PATCH 10/21] =?UTF-8?q?seed=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data/seed.js | 12 ------------ package.json | 3 +++ {data => prisma}/mock.js | 0 prisma/seed.js | 22 ++++++++++++++++++++++ 4 files changed, 25 insertions(+), 12 deletions(-) delete mode 100644 data/seed.js rename {data => prisma}/mock.js (100%) create mode 100644 prisma/seed.js diff --git a/data/seed.js b/data/seed.js deleted file mode 100644 index b2fd350..0000000 --- a/data/seed.js +++ /dev/null @@ -1,12 +0,0 @@ -import * as dotenv from "dotenv"; -import mongoose from "mongoose"; -import Product from "../models/product.js"; -import data from "./mock.js"; - -dotenv.config(); -mongoose.connect(process.env.DATABASE_URL); - -await Product.deleteMany({}); -await Product.insertMany(data); - -mongoose.connection.close(); diff --git a/package.json b/package.json index 7d7a89b..b61b442 100644 --- a/package.json +++ b/package.json @@ -15,5 +15,8 @@ "scripts": { "dev": "nodemon app.js", "start": "node app.js" + }, + "prisma": { + "seed": "node prisma/seed.js" } } diff --git a/data/mock.js b/prisma/mock.js similarity index 100% rename from data/mock.js rename to prisma/mock.js diff --git a/prisma/seed.js b/prisma/seed.js new file mode 100644 index 0000000..5e36868 --- /dev/null +++ b/prisma/seed.js @@ -0,0 +1,22 @@ +import { PrismaClient } from "@prisma/client"; +import data from "./mock.js"; +const prisma = new PrismaClient(); + +async function main() { + await prisma.product.deleteMany(); + + await prisma.product.createMany({ + data, + skipDuplicates: true, + }); +} + +main() + .then(async () => { + await prisma.$disconnect(); + }) + .catch(async (e) => { + console.error(e); + await prisma.$disconnect(); + process.exit(1); + }); From ca82e288df37d842203d82c100a78b175a5252b2 Mon Sep 17 00:00:00 2001 From: aowjarkwk Date: Wed, 22 May 2024 16:51:30 +0900 Subject: [PATCH 11/21] =?UTF-8?q?=EC=BF=BC=EB=A6=AC=20=ED=8C=8C=EB=9D=BC?= =?UTF-8?q?=EB=AF=B8=ED=84=B0=20=EC=B2=98=EB=A6=AC=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20-=20http,=20mock=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=20=EC=9C=84=EC=B9=98=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.js | 87 +++++++++++++++++++++++++++------------------- http/products.http | 12 +++---- models/product.js | 46 ------------------------ prisma/mock.js | 2 +- requests.http | 52 --------------------------- 5 files changed, 59 insertions(+), 140 deletions(-) delete mode 100644 models/product.js delete mode 100644 requests.http diff --git a/app.js b/app.js index 7bfa0d3..ea2f706 100644 --- a/app.js +++ b/app.js @@ -26,7 +26,36 @@ const asyncHandler = (handler) => { app.get( "/products", asyncHandler(async (req, res) => { - const products = await prisma.product.findMany(); + /** + * 쿼리 파라미터 + * - offset : 가져올 데이터의 시작 지점 + * - limit : 한 번에 가져올 데이터의 개수 + * - orderBy : 정렬 기준 favorite, recent (기본값: recent) + * - keyword : 검색 키워드 + */ + const { offset = 0, limit = 10, orderBy = "recent", keyword = "" } = req.query; + const order = orderBy === "favorite" ? { favoriteCount: "desc" } : { createdAt: "desc" }; + const products = await prisma.product.findMany({ + orderBy: order, + skip: parseInt(offset), + take: parseInt(limit), + where: { + OR: [ + { + name: { + contains: keyword, + mode: "insensitive", + }, + }, + { + description: { + contains: keyword, + mode: "insensitive", + }, + }, + ], + }, + }); res.send(products); }) ); @@ -105,22 +134,17 @@ app.delete( app.patch( "/products/:id/like", asyncHandler(async (req, res) => { - const product = await Product.findById(req.params.id); - - if (!product) { - res.status(404).send({ message: "존재하지 않는 상품입니다." }); - return; - } - - if (product.isFavorite) { - res.status(400).send({ message: "이미 좋아요 처리된 상품입니다." }); - return; - } - - product.favoriteCount += 1; - product.isFavorite = true; - - await product.save(); + const product = await prisma.product.update({ + where: { + id: req.params.id, + }, + data: { + favoriteCount: { + increment: 1, + }, + isFavorite: true, + }, + }); res.send(product); }) @@ -130,24 +154,17 @@ app.patch( app.patch( "/products/:id/unlike", asyncHandler(async (req, res) => { - const product = await Product.findById(req.params.id); - - if (!product) { - res.status(404).send({ message: "존재하지 않는 상품입니다." }); - return; - } - - if (!product.isFavorite) { - res.status(400).send({ message: "아직 좋아요 처리되지 않은 상품입니다." }); - return; - } - - if (product.favoriteCount > 0) { - product.favoriteCount -= 1; - } - product.isFavorite = false; - - await product.save(); + const product = await prisma.product.update({ + where: { + id: req.params.id, + }, + data: { + favoriteCount: { + decrement: 1, + }, + isFavorite: false, + }, + }); res.send(product); }) diff --git a/http/products.http b/http/products.http index c56456d..ee530c6 100644 --- a/http/products.http +++ b/http/products.http @@ -2,7 +2,7 @@ GET http://localhost:3000/products ### -GET http://localhost:3000/products?offset=0&limit=0&keyword=불곰 +GET http://localhost:3000/products?offset=1&limit=2&keyword=판다&orderBy=favorite ### @@ -10,7 +10,7 @@ GET http://localhost:3000/products?keyword=판다 ### -GET http://localhost:3000/products/8edc68a8-8361-4411-bb9f-dcb1a4c3d0de +GET http://localhost:3000/products/69c4bd5b-1281-4d6e-99de-fc18feed6de7 ### @@ -28,7 +28,7 @@ Content-Type: application/json ### -PATCH http://localhost:3000/products/8edc68a8-8361-4411-bb9f-dcb1a4c3d0de +PATCH http://localhost:3000/products/69c4bd5b-1281-4d6e-99de-fc18feed6de7 Content-Type: application/json { @@ -41,12 +41,12 @@ Content-Type: application/json ### -DELETE http://localhost:3000/products/8edc68a8-8361-4411-bb9f-dcb1a4c3d0de +DELETE http://localhost:3000/products/69c4bd5b-1281-4d6e-99de-fc18feed6de7 ### -PATCH http://localhost:3000/products/664d3eb1c4d678fe7647c427/like +PATCH http://localhost:3000/products/69c4bd5b-1281-4d6e-99de-fc18feed6de7/like ### -PATCH http://localhost:3000/products/664d3eb1c4d678fe7647c427/unlike +PATCH http://localhost:3000/products/69c4bd5b-1281-4d6e-99de-fc18feed6de7/unlike diff --git a/models/product.js b/models/product.js deleted file mode 100644 index dae54d8..0000000 --- a/models/product.js +++ /dev/null @@ -1,46 +0,0 @@ -import mongoose from "mongoose"; - -const ProductSchema = new mongoose.Schema( - { - name: { - type: String, - required: true, - }, - description: { - type: String, - }, - price: { - type: Number, - required: true, - default: 0, - }, - tags: { - type: [String], - required: true, - }, - images: { - type: [String], - required: true, - }, - favoriteCount: { - type: Number, - default: 0, - }, - isFavorite: { - type: Boolean, - required: true, - default: false, - }, - ownerId: { - type: Number, - required: true, - }, - }, - { - timestamps: true, - } -); - -const Product = mongoose.model("Product", ProductSchema); - -export default Product; diff --git a/prisma/mock.js b/prisma/mock.js index be18f37..b9d8c76 100644 --- a/prisma/mock.js +++ b/prisma/mock.js @@ -1,6 +1,6 @@ const data = [ { - favoriteCount: 0, + favoriteCount: 7, ownerId: 1, images: ["https://sitem.ssgcdn.com/62/11/49/item/1000559491162_i1_1100.jpg"], tags: ["판다인형", "인형", "판다"], diff --git a/requests.http b/requests.http deleted file mode 100644 index c2aae31..0000000 --- a/requests.http +++ /dev/null @@ -1,52 +0,0 @@ -GET http://localhost:3000/products - -### - -GET http://localhost:3000/products?offset=0&limit=0&keyword=불곰 - -### - -GET http://localhost:3000/products?keyword=판다 - -### - -GET http://localhost:3000/products/664d393f71f073e051e9aaca - -### - -POST http://localhost:3000/products -Content-Type: application/json - -{ - "name": "판다랑 불곰 교환원해요", - "description": "세종시청에서 교환원합니다.", - "price": 20000, - "tags": ["판다", "불곰"], - "images": ["https://www.wishbucket.io/_next/image?url=https%3A%2F%2Fd2gfz7wkiigkmv.cloudfront.net%2Fpickin%2F2%2F1%2F2%2FHereyhSJRMOmUw7I5uWAxg&w=640&q=75","https://wimg.mk.co.kr/meet/2021/09/image_listtop_2021_854860_1630738087.jpg"], - "ownerId":1 -} - -### - -PATCH http://localhost:3000/products/664d393f71f073e051e9aaca -Content-Type: application/json - -{ - "name":"판다 안팔려서 안판다", - "description":"안판다고 했지만 사실은 판다", - "price":7000, - "tags":["판다","안판다"] -} - - -### - -DELETE http://localhost:3000/products/664d393f71f073e051e9aaca - -### -PATCH http://localhost:3000/products/664d3eb1c4d678fe7647c427/like - -### - -PATCH http://localhost:3000/products/664d3eb1c4d678fe7647c427/unlike - From 8000c77d46f42d1fe0eefa56a5129e38ae5318bd Mon Sep 17 00:00:00 2001 From: aowjarkwk Date: Wed, 22 May 2024 17:21:49 +0900 Subject: [PATCH 12/21] =?UTF-8?q?=EC=98=A4=EB=A5=98=20=EC=B2=98=EB=A6=AC?= =?UTF-8?q?=20-=20=EC=A4=91=EA=B3=A0=EB=A7=88=EC=BC=93=20API=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.js | 79 +++++++++++++------ http/products.http | 2 + .../20240522094218_init_article/migration.sql | 14 ++++ prisma/mockArticle.js | 12 +++ prisma/schema.prisma | 12 +++ prisma/seed.js | 8 ++ structs.js | 12 +++ 7 files changed, 117 insertions(+), 22 deletions(-) create mode 100644 prisma/migrations/20240522094218_init_article/migration.sql create mode 100644 prisma/mockArticle.js create mode 100644 structs.js diff --git a/app.js b/app.js index ea2f706..3340c31 100644 --- a/app.js +++ b/app.js @@ -1,5 +1,7 @@ -import { PrismaClient } from "@prisma/client"; +import { Prisma, PrismaClient } from "@prisma/client"; import express from "express"; +import { assert } from "superstruct"; +import { CreateProduct, PatchProduct } from "./structs.js"; const prisma = new PrismaClient(); const app = express(); @@ -11,9 +13,9 @@ const asyncHandler = (handler) => { try { await handler(req, res); } catch (e) { - if (e.name === "ValidationError") { + if (e.name === "StructError" || e instanceof Prisma.PrismaClientValidationError) { res.status(400).send({ message: e.message }); - } else if (e.name === "CastError") { + } else if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === "P2025") { res.status(404).send({ message: "존재하지 않는 상품입니다." }); } else { res.status(500).send({ message: "서버 에러입니다." }); @@ -65,15 +67,11 @@ app.get( "/products/:id", asyncHandler(async (req, res) => { const { id } = req.params; - const product = await prisma.product.findUnique({ + const product = await prisma.product.findUniqueOrThrow({ where: { id, }, }); - if (!product) { - res.status(404).send({ message: "존재하지 않는 상품입니다." }); - return; - } res.send(product); }) @@ -83,6 +81,7 @@ app.get( app.post( "/products", asyncHandler(async (req, res) => { + assert(req.query, CreateProduct); const product = await prisma.product.create({ data: req.body, }); @@ -94,6 +93,7 @@ app.post( app.patch( "/products/:id", asyncHandler(async (req, res) => { + assert(req.body, PatchProduct); const { id } = req.params; const product = await prisma.product.update({ where: { @@ -101,10 +101,6 @@ app.patch( }, data: req.body, }); - if (!product) { - res.status(404).send({ message: "존재하지 않는 상품입니다." }); - return; - } res.send(product); }) @@ -115,17 +111,12 @@ app.delete( "/products/:id", asyncHandler(async (req, res) => { const { id } = req.params; - const product = await prisma.product.delete({ + await prisma.product.delete({ where: { id, }, }); - if (!product) { - res.status(404).send({ message: "존재하지 않는 상품입니다." }); - return; - } - res.sendStatus(204); }) ); @@ -134,7 +125,18 @@ app.delete( app.patch( "/products/:id/like", asyncHandler(async (req, res) => { - const product = await prisma.product.update({ + const product = await prisma.product.findUniqueOrThrow({ + where: { + id: req.params.id, + }, + }); + + if (product.isFavorite) { + res.status(400).send({ message: "이미 좋아요 처리된 상품입니다." }); + return; + } + + const updatedProduct = await prisma.product.update({ where: { id: req.params.id, }, @@ -146,7 +148,7 @@ app.patch( }, }); - res.send(product); + res.send(updatedProduct); }) ); @@ -154,7 +156,18 @@ app.patch( app.patch( "/products/:id/unlike", asyncHandler(async (req, res) => { - const product = await prisma.product.update({ + const product = await prisma.product.findUniqueOrThrow({ + where: { + id: req.params.id, + }, + }); + + if (!product.isFavorite) { + res.status(400).send({ message: "아직 좋아요 처리되지 않은 상품입니다." }); + return; + } + + const updatedProduct = await prisma.product.update({ where: { id: req.params.id, }, @@ -166,7 +179,29 @@ app.patch( }, }); - res.send(product); + res.send(updatedProduct); + }) +); + +// 게시글 조회 +app.get( + "/articles", + asyncHandler(async (req, res) => { + /** + * 쿼리 파라미터 + * - offset : 가져올 데이터의 시작 지점 + * - limit : 한 번에 가져올 데이터의 개수 + * - orderBy : 정렬 기준 favorite, recent (기본값: recent) + * - keyword : 검색 키워드 + */ + const { offset = 0, limit = 10, orderBy = "recent", keyword = "" } = req.query; + const order = orderBy === "favorite" ? { favoriteCount: "desc" } : { createdAt: "desc" }; + const articles = await prisma.article.findMany({ + orderBy: order, + skip: parseInt(offset), + take: parseInt(limit), + }); + res.send(articles); }) ); diff --git a/http/products.http b/http/products.http index ee530c6..6700304 100644 --- a/http/products.http +++ b/http/products.http @@ -50,3 +50,5 @@ PATCH http://localhost:3000/products/69c4bd5b-1281-4d6e-99de-fc18feed6de7/like PATCH http://localhost:3000/products/69c4bd5b-1281-4d6e-99de-fc18feed6de7/unlike +### +GET http://localhost:3000/articles \ No newline at end of file diff --git a/prisma/migrations/20240522094218_init_article/migration.sql b/prisma/migrations/20240522094218_init_article/migration.sql new file mode 100644 index 0000000..78aea67 --- /dev/null +++ b/prisma/migrations/20240522094218_init_article/migration.sql @@ -0,0 +1,14 @@ +-- CreateTable +CREATE TABLE "Article" ( + "id" TEXT NOT NULL, + "title" TEXT NOT NULL, + "content" TEXT NOT NULL, + "imageUrl" TEXT, + "likeCount" INTEGER NOT NULL DEFAULT 0, + "isLiked" BOOLEAN NOT NULL DEFAULT false, + "writer" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Article_pkey" PRIMARY KEY ("id") +); diff --git a/prisma/mockArticle.js b/prisma/mockArticle.js new file mode 100644 index 0000000..2c61221 --- /dev/null +++ b/prisma/mockArticle.js @@ -0,0 +1,12 @@ +const data = [ + { + title: "판다인형 구매 후기", + content: "판다인형 구매 후기입니다.", + imageUrl: "https://sitem.ssgcdn.com/62/11/49/item/1000559491162_i1_1100.jpg", + likeCount: 7, + isLiked: false, + writer: "판다인형 수집가", + }, +]; + +export default data; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 03d286e..0f30792 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -26,3 +26,15 @@ model Product { createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } + +model Article { + id String @id @default(uuid()) + title String + content String + imageUrl String? + likeCount Int @default(0) + isLiked Boolean @default(false) + writer String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} diff --git a/prisma/seed.js b/prisma/seed.js index 5e36868..23c5772 100644 --- a/prisma/seed.js +++ b/prisma/seed.js @@ -1,5 +1,6 @@ import { PrismaClient } from "@prisma/client"; import data from "./mock.js"; +import article from "./mockArticle.js"; const prisma = new PrismaClient(); async function main() { @@ -9,6 +10,13 @@ async function main() { data, skipDuplicates: true, }); + + await prisma.article.deleteMany(); + + await prisma.article.createMany({ + data: article, + skipDuplicates: true, + }); } main() diff --git a/structs.js b/structs.js new file mode 100644 index 0000000..4fa99f5 --- /dev/null +++ b/structs.js @@ -0,0 +1,12 @@ +import * as s from "superstruct"; + +export const CreateProduct = s.object({ + ownerId: s.number(), + images: s.array(s.string()), + tags: s.array(s.string()), + price: s.refine(s.number(), (price) => price > 0 && price < 1000000000), + description: s.string(), + name: s.string(), +}); + +export const PatchProduct = s.partial(CreateProduct); From 16a5a58ffaa40a0df1170843c68c34e4c8caa82a Mon Sep 17 00:00:00 2001 From: aowjarkwk Date: Thu, 23 May 2024 08:08:40 +0900 Subject: [PATCH 13/21] =?UTF-8?q?=EA=B2=8C=EC=8B=9C=EA=B8=80=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=20=EC=A1=B0=ED=9A=8C,=20=EA=B2=8C=EC=8B=9C=EA=B8=80?= =?UTF-8?q?=20=EC=83=81=EC=84=B8=20=EC=A1=B0=ED=9A=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.js | 66 +++++++++++++++++++++++++++++++++++++++---- http/articles.http | 8 ++++++ http/products.http | 19 +++++++------ prisma/mockArticle.js | 16 +++++++++++ 4 files changed, 94 insertions(+), 15 deletions(-) create mode 100644 http/articles.http diff --git a/app.js b/app.js index 3340c31..a14b669 100644 --- a/app.js +++ b/app.js @@ -72,7 +72,6 @@ app.get( id, }, }); - res.send(product); }) ); @@ -183,7 +182,7 @@ app.patch( }) ); -// 게시글 조회 +// 게시글 목록 조회 app.get( "/articles", asyncHandler(async (req, res) => { @@ -191,17 +190,72 @@ app.get( * 쿼리 파라미터 * - offset : 가져올 데이터의 시작 지점 * - limit : 한 번에 가져올 데이터의 개수 - * - orderBy : 정렬 기준 favorite, recent (기본값: recent) - * - keyword : 검색 키워드 + * - orderBy : 정렬 기준 like, recent (기본값: recent) */ const { offset = 0, limit = 10, orderBy = "recent", keyword = "" } = req.query; - const order = orderBy === "favorite" ? { favoriteCount: "desc" } : { createdAt: "desc" }; + const order = orderBy === "like" ? { likeCount: "desc" } : { createdAt: "desc" }; const articles = await prisma.article.findMany({ + select: { + id: true, + title: true, + content: true, + imageUrl: true, + createdAt: true, + writer: true, + }, orderBy: order, skip: parseInt(offset), take: parseInt(limit), + where: { + OR: [ + { + title: { + contains: keyword, + mode: "insensitive", + }, + }, + { + content: { + contains: keyword, + mode: "insensitive", + }, + }, + ], + }, + }); + // 좋아요가 많은 상위 4개의 글 조회 + const bestArticles = await prisma.article.findMany({ + orderBy: { + likeCount: "desc", + }, + take: 4, + }); + + res.send({ articles, bestArticles }); + }) +); + +// 게시글 상세 조회 +app.get( + "/articles/:id", + asyncHandler(async (req, res) => { + const { id } = req.params; + const article = await prisma.article.findUniqueOrThrow({ + where: { + id, + }, + select: { + id: true, + title: true, + content: true, + imageUrl: true, + createdAt: true, + likeCount: true, + isLiked: true, + writer: true, + }, }); - res.send(articles); + res.send(article); }) ); diff --git a/http/articles.http b/http/articles.http new file mode 100644 index 0000000..78651d2 --- /dev/null +++ b/http/articles.http @@ -0,0 +1,8 @@ +# 게시글 목록 조회 +GET http://localhost:3000/articles?&limit=10&&orderBy=like + +### +# 게시글 상세 조회 + +GET http://localhost:3000/articles/b66f0adf-0bc1-4bf7-a8bb-6fc8141fa56d + diff --git a/http/products.http b/http/products.http index 6700304..610fcb3 100644 --- a/http/products.http +++ b/http/products.http @@ -1,19 +1,22 @@ + +# 상품 조회 쿼리 x GET http://localhost:3000/products ### +# 상품 조회 쿼리 o GET http://localhost:3000/products?offset=1&limit=2&keyword=판다&orderBy=favorite ### - +# 상품 조회 검색어 테스트 GET http://localhost:3000/products?keyword=판다 ### - +# 상품 상세 조회 GET http://localhost:3000/products/69c4bd5b-1281-4d6e-99de-fc18feed6de7 ### - +# 상품 등록 POST http://localhost:3000/products Content-Type: application/json @@ -27,7 +30,7 @@ Content-Type: application/json } ### - +# 상품 수정 PATCH http://localhost:3000/products/69c4bd5b-1281-4d6e-99de-fc18feed6de7 Content-Type: application/json @@ -38,17 +41,15 @@ Content-Type: application/json "tags":["판다","안판다"] } - ### - +# 상품 삭제 DELETE http://localhost:3000/products/69c4bd5b-1281-4d6e-99de-fc18feed6de7 ### +# 상품 좋아요 PATCH http://localhost:3000/products/69c4bd5b-1281-4d6e-99de-fc18feed6de7/like ### - +# 상품 좋아요 취소 PATCH http://localhost:3000/products/69c4bd5b-1281-4d6e-99de-fc18feed6de7/unlike -### -GET http://localhost:3000/articles \ No newline at end of file diff --git a/prisma/mockArticle.js b/prisma/mockArticle.js index 2c61221..54630c3 100644 --- a/prisma/mockArticle.js +++ b/prisma/mockArticle.js @@ -7,6 +7,22 @@ const data = [ isLiked: false, writer: "판다인형 수집가", }, + { + title: "판다인형 판매 후기", + content: "판다인형 판매 후기입니다.", + imageUrl: "https://sitem.ssgcdn.com/62/11/49/item/1000559491162_i1_1100.jpg", + likeCount: 2, + isLiked: true, + writer: "판다인형 중개인", + }, + { + title: "불곰인형 구하는 곳 아시는분", + content: "불곰인형 구하는 곳 아시는분 계신가요?", + imageUrl: "https://wimg.mk.co.kr/meet/2021/09/image_listtop_2021_854860_1630738087.jpg", + likeCount: 3, + isLiked: false, + writer: "불곰인형 수집가", + }, ]; export default data; From 30b29804f8c67c3ad94dc35133084fb4bf5ec25e Mon Sep 17 00:00:00 2001 From: aowjarkwk Date: Thu, 23 May 2024 08:28:47 +0900 Subject: [PATCH 14/21] =?UTF-8?q?=EC=83=81=ED=92=88=20=EB=93=B1=EB=A1=9D?= =?UTF-8?q?=20api=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.js | 5 ++--- structs.js | 4 +++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/app.js b/app.js index a14b669..7fd7f10 100644 --- a/app.js +++ b/app.js @@ -2,10 +2,9 @@ import { Prisma, PrismaClient } from "@prisma/client"; import express from "express"; import { assert } from "superstruct"; import { CreateProduct, PatchProduct } from "./structs.js"; -const prisma = new PrismaClient(); +const prisma = new PrismaClient(); const app = express(); - app.use(express.json()); const asyncHandler = (handler) => { @@ -80,7 +79,7 @@ app.get( app.post( "/products", asyncHandler(async (req, res) => { - assert(req.query, CreateProduct); + assert(req.body, CreateProduct); const product = await prisma.product.create({ data: req.body, }); diff --git a/structs.js b/structs.js index 4fa99f5..ab716f8 100644 --- a/structs.js +++ b/structs.js @@ -1,10 +1,12 @@ import * as s from "superstruct"; +const PositivePrice = s.refine(s.number(), "PositivePrice", (value) => value > 0 && value < 1000000000); + export const CreateProduct = s.object({ ownerId: s.number(), images: s.array(s.string()), tags: s.array(s.string()), - price: s.refine(s.number(), (price) => price > 0 && price < 1000000000), + price: PositivePrice, description: s.string(), name: s.string(), }); From ee6daef67f45c7a57534dffae7a770e907a08145 Mon Sep 17 00:00:00 2001 From: aowjarkwk Date: Thu, 23 May 2024 08:46:08 +0900 Subject: [PATCH 15/21] =?UTF-8?q?=EA=B2=8C=EC=8B=9C=EA=B8=80=20=EB=93=B1?= =?UTF-8?q?=EB=A1=9D,=20=EC=88=98=EC=A0=95,=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.js | 48 ++++++++++++++++++++++++++++++++++++++++++++-- http/articles.http | 25 ++++++++++++++++++++++++ structs.js | 7 +++++++ 3 files changed, 78 insertions(+), 2 deletions(-) diff --git a/app.js b/app.js index 7fd7f10..f9b6758 100644 --- a/app.js +++ b/app.js @@ -1,7 +1,7 @@ import { Prisma, PrismaClient } from "@prisma/client"; import express from "express"; import { assert } from "superstruct"; -import { CreateProduct, PatchProduct } from "./structs.js"; +import { CreateArticle, CreateProduct, PatchArticle, PatchProduct } from "./structs.js"; const prisma = new PrismaClient(); const app = express(); @@ -15,7 +15,7 @@ const asyncHandler = (handler) => { if (e.name === "StructError" || 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: "존재하지 않는 상품입니다." }); + res.status(404).send({ message: "존재하지 않는 게시글입니다." }); } else { res.status(500).send({ message: "서버 에러입니다." }); } @@ -258,4 +258,48 @@ app.get( }) ); +// 게시글 등록 +app.post( + "/articles", + asyncHandler(async (req, res) => { + assert(req.body, CreateArticle); + const article = await prisma.article.create({ + data: req.body, + }); + res.status(201).send(article); + }) +); + +// 게시글 수정 +app.patch( + "/articles/:id", + asyncHandler(async (req, res) => { + assert(req.body, PatchArticle); + const { id } = req.params; + const article = await prisma.article.update({ + where: { + id, + }, + data: req.body, + }); + + res.send(article); + }) +); + +// 게시글 삭제 +app.delete( + "/articles/:id", + asyncHandler(async (req, res) => { + const { id } = req.params; + await prisma.article.delete({ + where: { + id, + }, + }); + + res.sendStatus(204); + }) +); + app.listen(process.env.PORT || 3000, () => console.log("Server Started")); diff --git a/http/articles.http b/http/articles.http index 78651d2..0b128a4 100644 --- a/http/articles.http +++ b/http/articles.http @@ -6,3 +6,28 @@ GET http://localhost:3000/articles?&limit=10&&orderBy=like GET http://localhost:3000/articles/b66f0adf-0bc1-4bf7-a8bb-6fc8141fa56d +### +# 게시글 등록 +POST http://localhost:3000/articles +Content-Type: application/json + +{ + "title": "제가 아끼는 티모 인형입니다", + "content": "버섯 농사 짓는 모습이 너무 깜찍하지않나요?", + "imageUrl": "https://cdn.011st.com/11dims/resize/600x600/quality/75/11src/product/5575072075/B.jpg?51000000", + "writer": "티모매니아" +} + +### +# 게시글 수정 +PATCH http://localhost:3000/articles/7a640d97-ba35-4bae-bf9e-b6f30bf0ebb0 +Content-Type: application/json + +{ + "title":"판다 대박이네요", + "content":"대나무를 잘 먹네요 ㄷㄷ" +} + +### +# 게시글 삭제 +DELETE http://localhost:3000/articles/7a640d97-ba35-4bae-bf9e-b6f30bf0ebb0 diff --git a/structs.js b/structs.js index ab716f8..b550f0e 100644 --- a/structs.js +++ b/structs.js @@ -12,3 +12,10 @@ export const CreateProduct = s.object({ }); export const PatchProduct = s.partial(CreateProduct); + +export const CreateArticle = s.object({ + title: s.string(), + content: s.string(), +}); + +export const PatchArticle = s.partial(CreateArticle); From d632f32471f1275a1a96ac1c90f527caf8223700 Mon Sep 17 00:00:00 2001 From: aowjarkwk Date: Thu, 23 May 2024 08:56:24 +0900 Subject: [PATCH 16/21] =?UTF-8?q?=EA=B2=8C=EC=8B=9C=EA=B8=80=20=EC=A2=8B?= =?UTF-8?q?=EC=95=84=EC=9A=94,=20=EC=A2=8B=EC=95=84=EC=9A=94=20=EC=B7=A8?= =?UTF-8?q?=EC=86=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.js | 62 ++++++++++++++++++++++++++++++++++++++++++++++ http/articles.http | 9 +++++++ 2 files changed, 71 insertions(+) diff --git a/app.js b/app.js index f9b6758..8f98a0f 100644 --- a/app.js +++ b/app.js @@ -302,4 +302,66 @@ app.delete( }) ); +// 게시글 좋아요 +app.patch( + "/articles/:id/like", + asyncHandler(async (req, res) => { + const article = await prisma.article.findUniqueOrThrow({ + where: { + id: req.params.id, + }, + }); + + if (article.isLiked) { + res.status(400).send({ message: "이미 좋아요 처리된 게시글입니다." }); + return; + } + + const updatedArticle = await prisma.article.update({ + where: { + id: req.params.id, + }, + data: { + likeCount: { + increment: 1, + }, + isLiked: true, + }, + }); + + res.send(updatedArticle); + }) +); + +// 게시글 좋아요 취소 +app.patch( + "/articles/:id/unlike", + asyncHandler(async (req, res) => { + const article = await prisma.article.findUniqueOrThrow({ + where: { + id: req.params.id, + }, + }); + + if (!article.isLiked) { + res.status(400).send({ message: "아직 좋아요 처리되지 않은 게시글입니다." }); + return; + } + + const updatedArticle = await prisma.article.update({ + where: { + id: req.params.id, + }, + data: { + likeCount: { + decrement: 1, + }, + isLiked: false, + }, + }); + + res.send(updatedArticle); + }) +); + app.listen(process.env.PORT || 3000, () => console.log("Server Started")); diff --git a/http/articles.http b/http/articles.http index 0b128a4..37bcd07 100644 --- a/http/articles.http +++ b/http/articles.http @@ -31,3 +31,12 @@ Content-Type: application/json ### # 게시글 삭제 DELETE http://localhost:3000/articles/7a640d97-ba35-4bae-bf9e-b6f30bf0ebb0 + +### +# 게시글 좋아요 +PATCH http://localhost:3000/articles/7757f3a4-0075-47e0-a9e5-b38500948b8e/like + +### +# 게시글 좋아요 취소 +PATCH http://localhost:3000/articles/7757f3a4-0075-47e0-a9e5-b38500948b8e/unlike + From 883250b00207cb1acde3b608509c3540a0d26402 Mon Sep 17 00:00:00 2001 From: aowjarkwk Date: Thu, 23 May 2024 09:38:53 +0900 Subject: [PATCH 17/21] =?UTF-8?q?=EB=8C=93=EA=B8=80=20=EB=AA=A8=EB=8D=B8,?= =?UTF-8?q?=20seed,=20mock=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../20240523002157_init_comment/migration.sql | 24 ++++++++ prisma/mock.js | 55 ++++++++++++++++++- prisma/mockArticle.js | 28 ---------- prisma/schema.prisma | 33 ++++++++--- prisma/seed.js | 13 +++-- 5 files changed, 111 insertions(+), 42 deletions(-) create mode 100644 prisma/migrations/20240523002157_init_comment/migration.sql delete mode 100644 prisma/mockArticle.js diff --git a/prisma/migrations/20240523002157_init_comment/migration.sql b/prisma/migrations/20240523002157_init_comment/migration.sql new file mode 100644 index 0000000..645652d --- /dev/null +++ b/prisma/migrations/20240523002157_init_comment/migration.sql @@ -0,0 +1,24 @@ +-- CreateTable +CREATE TABLE "Comment" ( + "id" TEXT NOT NULL, + "content" TEXT NOT NULL, + "writer" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "productId" TEXT, + "articleId" TEXT, + + CONSTRAINT "Comment_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX "Comment_productId_idx" ON "Comment"("productId"); + +-- CreateIndex +CREATE INDEX "Comment_articleId_idx" ON "Comment"("articleId"); + +-- AddForeignKey +ALTER TABLE "Comment" ADD CONSTRAINT "Comment_productId_fkey" FOREIGN KEY ("productId") REFERENCES "Product"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Comment" ADD CONSTRAINT "Comment_articleId_fkey" FOREIGN KEY ("articleId") REFERENCES "Article"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/prisma/mock.js b/prisma/mock.js index b9d8c76..43c170d 100644 --- a/prisma/mock.js +++ b/prisma/mock.js @@ -1,5 +1,6 @@ -const data = [ +export const PRODUCTS = [ { + id: "377ce06c-23c8-46de-b86f-2cfd43d41cbc", favoriteCount: 7, ownerId: 1, images: ["https://sitem.ssgcdn.com/62/11/49/item/1000559491162_i1_1100.jpg"], @@ -9,6 +10,7 @@ const data = [ name: "판다인형", }, { + id: "d4e8c9a0-5d45-4c9f-9b4b-7626f3c9c9a9", favoriteCount: 2, ownerId: 2, images: [ @@ -21,4 +23,53 @@ const data = [ }, ]; -export default data; +export const ARTICLES = [ + { + id: "2c027764-d7ef-4a94-8399-f15ffbf8f4da", + title: "판다인형 구매 후기", + content: "판다인형 구매 후기입니다.", + imageUrl: "https://sitem.ssgcdn.com/62/11/49/item/1000559491162_i1_1100.jpg", + likeCount: 7, + isLiked: false, + writer: "판다인형 수집가", + }, + { + id: "7c8b9d2e-5d45-4c9f-9b4b-7626f3c9c9a9", + title: "판다인형 판매 후기", + content: "판다인형 판매 후기입니다.", + imageUrl: "https://sitem.ssgcdn.com/62/11/49/item/1000559491162_i1_1100.jpg", + likeCount: 2, + isLiked: true, + writer: "판다인형 중개인", + }, + { + id: "287cb4c8-48c5-49e1-82fa-a1b9e2d7b4e3", + title: "불곰인형 구하는 곳 아시는분", + content: "불곰인형 구하는 곳 아시는분 계신가요?", + imageUrl: "https://wimg.mk.co.kr/meet/2021/09/image_listtop_2021_854860_1630738087.jpg", + likeCount: 3, + isLiked: false, + writer: "불곰인형 수집가", + }, +]; + +export const COMMENTS = [ + { + id: "1a2b3c4d-5e6f-7g8h-9i0j-1k2l3m4n5o6p", + content: "판다인형 너무 귀여워요!", + writer: "판다인형 수집가", + productId: "377ce06c-23c8-46de-b86f-2cfd43d41cbc", + }, + { + id: "2b3c4d5e-6f7g-8h9i-0j1k-2l3m4n5o6p7q", + content: "판다인형 너무 귀여워요!", + writer: "판다인형 중개인", + articleId: "2c027764-d7ef-4a94-8399-f15ffbf8f4da", + }, + { + id: "3c4d5e6f-7g8h-9i0j-1k2l-3m4n5o6p7q8r", + content: "불곰인형 너무 귀여워요!", + writer: "불곰인형 수집가", + articleId: "287cb4c8-48c5-49e1-82fa-a1b9e2d7b4e3", + }, +]; diff --git a/prisma/mockArticle.js b/prisma/mockArticle.js deleted file mode 100644 index 54630c3..0000000 --- a/prisma/mockArticle.js +++ /dev/null @@ -1,28 +0,0 @@ -const data = [ - { - title: "판다인형 구매 후기", - content: "판다인형 구매 후기입니다.", - imageUrl: "https://sitem.ssgcdn.com/62/11/49/item/1000559491162_i1_1100.jpg", - likeCount: 7, - isLiked: false, - writer: "판다인형 수집가", - }, - { - title: "판다인형 판매 후기", - content: "판다인형 판매 후기입니다.", - imageUrl: "https://sitem.ssgcdn.com/62/11/49/item/1000559491162_i1_1100.jpg", - likeCount: 2, - isLiked: true, - writer: "판다인형 중개인", - }, - { - title: "불곰인형 구하는 곳 아시는분", - content: "불곰인형 구하는 곳 아시는분 계신가요?", - imageUrl: "https://wimg.mk.co.kr/meet/2021/09/image_listtop_2021_854860_1630738087.jpg", - likeCount: 3, - isLiked: false, - writer: "불곰인형 수집가", - }, -]; - -export default data; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 0f30792..4b24178 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -14,27 +14,44 @@ datasource db { } model Product { - id String @id @default(uuid()) + id String @id @default(uuid()) name String description String price Float tags String[] images String[] - favoriteCount Int @default(0) - isFavorite Boolean @default(false) + favoriteCount Int @default(0) + isFavorite Boolean @default(false) ownerId Int - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + comments Comment[] } model Article { - id String @id @default(uuid()) + id String @id @default(uuid()) title String content String imageUrl String? - likeCount Int @default(0) - isLiked Boolean @default(false) + likeCount Int @default(0) + isLiked Boolean @default(false) + writer String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + comments Comment[] +} + +model Comment { + id String @id @default(uuid()) + content String writer String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt + product Product? @relation(fields: [productId], references: [id], onDelete: SetNull) + productId String? + article Article? @relation(fields: [articleId], references: [id], onDelete: SetNull) + articleId String? + + @@index([productId]) + @@index([articleId]) } diff --git a/prisma/seed.js b/prisma/seed.js index 23c5772..92f7f42 100644 --- a/prisma/seed.js +++ b/prisma/seed.js @@ -1,20 +1,25 @@ import { PrismaClient } from "@prisma/client"; -import data from "./mock.js"; -import article from "./mockArticle.js"; +import { ARTICLES, COMMENTS, PRODUCTS } from "./mock.js"; const prisma = new PrismaClient(); async function main() { await prisma.product.deleteMany(); await prisma.product.createMany({ - data, + data: PRODUCTS, skipDuplicates: true, }); await prisma.article.deleteMany(); await prisma.article.createMany({ - data: article, + data: ARTICLES, + skipDuplicates: true, + }); + await prisma.comment.deleteMany(); + + await prisma.comment.createMany({ + data: COMMENTS, skipDuplicates: true, }); } From fe528aa9633fb92df3a301684e4d732c8f0c3e95 Mon Sep 17 00:00:00 2001 From: aowjarkwk Date: Thu, 23 May 2024 10:06:42 +0900 Subject: [PATCH 18/21] =?UTF-8?q?=EC=A4=91=EA=B3=A0=EB=A7=88=EC=BC=93=20?= =?UTF-8?q?=EB=8C=93=EA=B8=80=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C,?= =?UTF-8?q?=20=EB=93=B1=EB=A1=9D,=20=EC=88=98=EC=A0=95,=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.js | 86 ++++++++++++++++++++++++++++++++++++++++++++++ http/products.http | 30 ++++++++++++++++ prisma/mock.js | 61 ++++++++++++++++++++++++++++++-- 3 files changed, 174 insertions(+), 3 deletions(-) diff --git a/app.js b/app.js index 8f98a0f..0fc92e2 100644 --- a/app.js +++ b/app.js @@ -364,4 +364,90 @@ app.patch( }) ); +// 중고마켓 댓글 목록 조회 +app.get( + "/products/:id/comments", + asyncHandler(async (req, res) => { + const { id } = req.params; + const { cursor } = req.query; + console.log(cursor); + let queryOptions = { + take: 10, + orderBy: { + createdAt: "desc", + }, + where: { + productId: id, + }, + select: { + id: true, + content: true, + createdAt: true, + writer: true, + }, + }; + + if (cursor) { + queryOptions = { + ...queryOptions, + cursor: { + id: cursor, + }, + skip: 1, + }; + } + + const comments = await prisma.comment.findMany(queryOptions); + res.send(comments); + }) +); + +// 중고마켓 댓글 등록 +app.post( + "/products/:id/comments", + asyncHandler(async (req, res) => { + const { id, cursor } = req.params; + + const comment = await prisma.comment.create({ + data: { + content: req.body.content, + writer: req.body.writer, + productId: id, + }, + }); + res.status(201).send(comment); + }) +); + +// 중고마켓 댓글 수정 +app.patch( + "/products/:id/comments/:commentId", + asyncHandler(async (req, res) => { + const { commentId } = req.params; + const comment = await prisma.comment.update({ + where: { + id: commentId, + }, + data: req.body, + }); + + res.send(comment); + }) +); + +// 중고마켓 댓글 삭제 +app.delete( + "/products/:id/comments/:commentId", + asyncHandler(async (req, res) => { + const { commentId } = req.params; + await prisma.comment.delete({ + where: { + id: commentId, + }, + }); + + res.sendStatus(204); + }) +); + app.listen(process.env.PORT || 3000, () => console.log("Server Started")); diff --git a/http/products.http b/http/products.http index 610fcb3..36dcd96 100644 --- a/http/products.http +++ b/http/products.http @@ -53,3 +53,33 @@ PATCH http://localhost:3000/products/69c4bd5b-1281-4d6e-99de-fc18feed6de7/like # 상품 좋아요 취소 PATCH http://localhost:3000/products/69c4bd5b-1281-4d6e-99de-fc18feed6de7/unlike +### +# 상품 댓글 조회 +GET http://localhost:3000/products/377ce06c-23c8-46de-b86f-2cfd43d41cbc/comments + +### +# 상품 댓글 커서 조회 +GET http://localhost:3000/products/377ce06c-23c8-46de-b86f-2cfd43d41cbc/comments?cursor=c45c56cf-d7d4-4592-885c-abcb56661bb5 + +### +# 상품 댓글 등록 +POST http://localhost:3000/products/377ce06c-23c8-46de-b86f-2cfd43d41cbc/comments +Content-Type: application/json + +{ + "content":"판다가 너무 귀여워요", + "writer":"판다사랑나라사랑" +} + +### +# 상품 댓글 수정 +PATCH http://localhost:3000/products/377ce06c-23c8-46de-b86f-2cfd43d41cbc/comments/512b432b-7618-46ea-a500-579daacc0d02 +Content-Type: application/json + +{ + "content":"판다가 너무 귀여워요 수정" +} + +### +# 상품 댓글 삭제 +DELETE http://localhost:3000/products/377ce06c-23c8-46de-b86f-2cfd43d41cbc/comments/512b432b-7618-46ea-a500-579daacc0d02 \ No newline at end of file diff --git a/prisma/mock.js b/prisma/mock.js index 43c170d..64ec75a 100644 --- a/prisma/mock.js +++ b/prisma/mock.js @@ -58,13 +58,68 @@ export const COMMENTS = [ id: "1a2b3c4d-5e6f-7g8h-9i0j-1k2l3m4n5o6p", content: "판다인형 너무 귀여워요!", writer: "판다인형 수집가", - productId: "377ce06c-23c8-46de-b86f-2cfd43d41cbc", + articleId: "2c027764-d7ef-4a94-8399-f15ffbf8f4da", }, { id: "2b3c4d5e-6f7g-8h9i-0j1k-2l3m4n5o6p7q", - content: "판다인형 너무 귀여워요!", + content: "판다인형 너무 귀여워요1", writer: "판다인형 중개인", - articleId: "2c027764-d7ef-4a94-8399-f15ffbf8f4da", + productId: "377ce06c-23c8-46de-b86f-2cfd43d41cbc", + }, + { + content: "판다인형 너무 귀여워요2", + writer: "판다인형 중개인", + productId: "377ce06c-23c8-46de-b86f-2cfd43d41cbc", + }, + { + content: "판다인형 너무 귀여워요3", + writer: "판다인형 중개인", + productId: "377ce06c-23c8-46de-b86f-2cfd43d41cbc", + }, + { + content: "판다인형 너무 귀여워요4", + writer: "판다인형 중개인", + productId: "377ce06c-23c8-46de-b86f-2cfd43d41cbc", + }, + { + content: "판다인형 너무 귀여워요5", + writer: "판다인형 중개인", + productId: "377ce06c-23c8-46de-b86f-2cfd43d41cbc", + }, + { + content: "판다인형 너무 귀여워요6", + writer: "판다인형 중개인", + productId: "377ce06c-23c8-46de-b86f-2cfd43d41cbc", + }, + { + content: "판다인형 너무 귀여워요7", + writer: "판다인형 중개인", + productId: "377ce06c-23c8-46de-b86f-2cfd43d41cbc", + }, + { + content: "판다인형 너무 귀여워요8", + writer: "판다인형 중개인", + productId: "377ce06c-23c8-46de-b86f-2cfd43d41cbc", + }, + { + content: "판다인형 너무 귀여워요9", + writer: "판다인형 중개인", + productId: "377ce06c-23c8-46de-b86f-2cfd43d41cbc", + }, + { + content: "판다인형 너무 귀여워요10", + writer: "판다인형 중개인", + productId: "377ce06c-23c8-46de-b86f-2cfd43d41cbc", + }, + { + content: "판다인형 너무 귀여워요11", + writer: "판다인형 중개인", + productId: "377ce06c-23c8-46de-b86f-2cfd43d41cbc", + }, + { + content: "판다인형 너무 귀여워요12", + writer: "판다인형 중개인", + productId: "377ce06c-23c8-46de-b86f-2cfd43d41cbc", }, { id: "3c4d5e6f-7g8h-9i0j-1k2l-3m4n5o6p7q8r", From 2de35b2aac43e865b7c0f91ac4a8db8379a99384 Mon Sep 17 00:00:00 2001 From: aowjarkwk Date: Thu, 23 May 2024 11:39:52 +0900 Subject: [PATCH 19/21] =?UTF-8?q?=EC=9E=90=EC=9C=A0=EA=B2=8C=EC=8B=9C?= =?UTF-8?q?=ED=8C=90=20=EB=8C=93=EA=B8=80=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C,=20=EB=93=B1=EB=A1=9D,=20=EC=88=98=EC=A0=95,=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.js | 96 +++++++++++++++++++++++++++++++++++++++++++--- http/articles.http | 35 ++++++++++++++++- structs.js | 7 ++++ 3 files changed, 132 insertions(+), 6 deletions(-) diff --git a/app.js b/app.js index 0fc92e2..1a02c69 100644 --- a/app.js +++ b/app.js @@ -1,7 +1,7 @@ import { Prisma, PrismaClient } from "@prisma/client"; import express from "express"; import { assert } from "superstruct"; -import { CreateArticle, CreateProduct, PatchArticle, PatchProduct } from "./structs.js"; +import { CreateArticle, CreateComment, CreateProduct, PatchArticle, PatchComment, PatchProduct } from "./structs.js"; const prisma = new PrismaClient(); const app = express(); @@ -370,7 +370,6 @@ app.get( asyncHandler(async (req, res) => { const { id } = req.params; const { cursor } = req.query; - console.log(cursor); let queryOptions = { take: 10, orderBy: { @@ -406,12 +405,12 @@ app.get( app.post( "/products/:id/comments", asyncHandler(async (req, res) => { - const { id, cursor } = req.params; + assert(req.body, CreateComment); + const { id } = req.params; const comment = await prisma.comment.create({ data: { - content: req.body.content, - writer: req.body.writer, + ...req.body, productId: id, }, }); @@ -423,6 +422,7 @@ app.post( app.patch( "/products/:id/comments/:commentId", asyncHandler(async (req, res) => { + assert(req.body, PatchComment); const { commentId } = req.params; const comment = await prisma.comment.update({ where: { @@ -450,4 +450,90 @@ app.delete( }) ); +// 자유게시판 댓글 목록 조회 +app.get( + "/articles/:id/comments", + asyncHandler(async (req, res) => { + const { id } = req.params; + const { cursor } = req.query; + let queryOptions = { + take: 10, + orderBy: { + createdAt: "desc", + }, + where: { + articleId: id, + }, + select: { + id: true, + content: true, + createdAt: true, + writer: true, + }, + }; + + if (cursor) { + queryOptions = { + ...queryOptions, + cursor: { + id: cursor, + }, + skip: 1, + }; + } + + const comments = await prisma.comment.findMany(queryOptions); + res.send(comments); + }) +); + +// 자유게시판 댓글 등록 +app.post( + "/articles/:id/comments", + asyncHandler(async (req, res) => { + assert(req.body, CreateComment); + const { id } = req.params; + + const comment = await prisma.comment.create({ + data: { + ...req.body, + articleId: id, + }, + }); + res.status(201).send(comment); + }) +); + +// 자유게시판 댓글 수정 +app.patch( + "/articles/:id/comments/:commentId", + asyncHandler(async (req, res) => { + assert(req.body, PatchComment); + const { commentId } = req.params; + const comment = await prisma.comment.update({ + where: { + id: commentId, + }, + data: req.body, + }); + + res.send(comment); + }) +); + +// 중고마켓 댓글 삭제 +app.delete( + "/articles/:id/comments/:commentId", + asyncHandler(async (req, res) => { + const { commentId } = req.params; + await prisma.comment.delete({ + where: { + id: commentId, + }, + }); + + res.sendStatus(204); + }) +); + app.listen(process.env.PORT || 3000, () => console.log("Server Started")); diff --git a/http/articles.http b/http/articles.http index 37bcd07..8b518af 100644 --- a/http/articles.http +++ b/http/articles.http @@ -30,7 +30,7 @@ Content-Type: application/json ### # 게시글 삭제 -DELETE http://localhost:3000/articles/7a640d97-ba35-4bae-bf9e-b6f30bf0ebb0 +DELETE http://localhost:3000/articles/2c027764-d7ef-4a94-8399-f15ffbf8f4da ### # 게시글 좋아요 @@ -40,3 +40,36 @@ PATCH http://localhost:3000/articles/7757f3a4-0075-47e0-a9e5-b38500948b8e/like # 게시글 좋아요 취소 PATCH http://localhost:3000/articles/7757f3a4-0075-47e0-a9e5-b38500948b8e/unlike + + + +### +# 자유게시판 댓글 조회 +GET http://localhost:3000/articles/2c027764-d7ef-4a94-8399-f15ffbf8f4da/comments + +### +# 자유게시판 댓글 커서 조회 +GET http://localhost:3000/articles/2c027764-d7ef-4a94-8399-f15ffbf8f4da/comments?cursor=43d35ef2-42f0-43be-b85c-f9614195bb39 + +### +# 자유게시판 댓글 등록 +POST http://localhost:3000/articles/287cb4c8-48c5-49e1-82fa-a1b9e2d7b4e3/comments +Content-Type: application/json + +{ + "content":"판다가 너무 귀여워요2", + "writer":"판다사랑나라사랑" +} + +### +# 자유게시판 댓글 수정 +PATCH http://localhost:3000/articles/2c027764-d7ef-4a94-8399-f15ffbf8f4da/comments/3c94cbe4-bd70-4c32-969a-225af0dd1e20 +Content-Type: application/json + +{ + "content":"판다가 너무 귀여워요 수정" +} + +### +# 자유게시판 댓글 삭제 +DELETE http://localhost:3000/articles/2c027764-d7ef-4a94-8399-f15ffbf8f4da/comments/3c94cbe4-bd70-4c32-969a-225af0dd1e20 \ No newline at end of file diff --git a/structs.js b/structs.js index b550f0e..1050ee2 100644 --- a/structs.js +++ b/structs.js @@ -19,3 +19,10 @@ export const CreateArticle = s.object({ }); export const PatchArticle = s.partial(CreateArticle); + +export const CreateComment = s.object({ + content: s.string(), + writer: s.string(), +}); + +export const PatchComment = s.partial(CreateComment); From 4d09d8c3d0a0e206749db3f30c31d6166c04d19a Mon Sep 17 00:00:00 2001 From: aowjarkwk Date: Thu, 23 May 2024 14:48:32 +0900 Subject: [PATCH 20/21] =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=9C=A0?= =?UTF-8?q?=ED=9A=A8=EC=84=B1=20=EA=B2=80=EC=82=AC,=20=EC=97=90=EB=9F=AC?= =?UTF-8?q?=EB=A9=94=EC=8B=9C=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.js | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/app.js b/app.js index 1a02c69..55feaa0 100644 --- a/app.js +++ b/app.js @@ -12,12 +12,19 @@ const asyncHandler = (handler) => { try { await handler(req, res); } catch (e) { - if (e.name === "StructError" || e instanceof Prisma.PrismaClientValidationError) { - res.status(400).send({ message: e.message }); + if (e.name === "StructError") { + const errors = e.failures().map((failure) => ({ + path: failure.path.join("."), + message: failure.message, + })); + res.status(400).json({ message: "유효성 검사 오류입니다.", errors }); + } else if (e instanceof Prisma.PrismaClientValidationError) { + res.status(400).json({ message: e.message }); } else if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === "P2025") { - res.status(404).send({ message: "존재하지 않는 게시글입니다." }); + res.status(404).json({ message: "존재하지 않는 게시글입니다." }); } else { - res.status(500).send({ message: "서버 에러입니다." }); + console.error(e); + res.status(500).json({ message: "서버 에러입니다." }); } } }; From 5e3948a9ce0f8e0c9e00585c360fd9aa8b03cb0d Mon Sep 17 00:00:00 2001 From: aowjarkwk Date: Thu, 23 May 2024 16:20:39 +0900 Subject: [PATCH 21/21] =?UTF-8?q?writer=20=ED=95=84=EB=93=9C=EB=A5=BC=20op?= =?UTF-8?q?tional=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- http/articles.http | 22 +++++++++---------- http/products.http | 16 +++++++------- .../migration.sql | 6 +++++ prisma/mock.js | 16 ++++++++++++++ prisma/schema.prisma | 6 ++--- structs.js | 4 +++- 6 files changed, 46 insertions(+), 24 deletions(-) create mode 100644 prisma/migrations/20240523072342_change_writer_to_optional/migration.sql diff --git a/http/articles.http b/http/articles.http index 8b518af..ad5c723 100644 --- a/http/articles.http +++ b/http/articles.http @@ -4,7 +4,7 @@ GET http://localhost:3000/articles?&limit=10&&orderBy=like ### # 게시글 상세 조회 -GET http://localhost:3000/articles/b66f0adf-0bc1-4bf7-a8bb-6fc8141fa56d +GET http://localhost:3000/articles/2c027764-d7ef-4a94-8399-f15ffbf8f4da ### # 게시글 등록 @@ -14,13 +14,12 @@ Content-Type: application/json { "title": "제가 아끼는 티모 인형입니다", "content": "버섯 농사 짓는 모습이 너무 깜찍하지않나요?", - "imageUrl": "https://cdn.011st.com/11dims/resize/600x600/quality/75/11src/product/5575072075/B.jpg?51000000", - "writer": "티모매니아" + "imageUrl": "https://cdn.011st.com/11dims/resize/600x600/quality/75/11src/product/5575072075/B.jpg?51000000" } ### # 게시글 수정 -PATCH http://localhost:3000/articles/7a640d97-ba35-4bae-bf9e-b6f30bf0ebb0 +PATCH http://localhost:3000/articles/2c027764-d7ef-4a94-8399-f15ffbf8f4da Content-Type: application/json { @@ -34,22 +33,22 @@ DELETE http://localhost:3000/articles/2c027764-d7ef-4a94-8399-f15ffbf8f4da ### # 게시글 좋아요 -PATCH http://localhost:3000/articles/7757f3a4-0075-47e0-a9e5-b38500948b8e/like +PATCH http://localhost:3000/articles/2c027764-d7ef-4a94-8399-f15ffbf8f4da/like ### # 게시글 좋아요 취소 -PATCH http://localhost:3000/articles/7757f3a4-0075-47e0-a9e5-b38500948b8e/unlike +PATCH http://localhost:3000/articles/2c027764-d7ef-4a94-8399-f15ffbf8f4da/unlike ### # 자유게시판 댓글 조회 -GET http://localhost:3000/articles/2c027764-d7ef-4a94-8399-f15ffbf8f4da/comments +GET http://localhost:3000/articles/287cb4c8-48c5-49e1-82fa-a1b9e2d7b4e3/comments ### # 자유게시판 댓글 커서 조회 -GET http://localhost:3000/articles/2c027764-d7ef-4a94-8399-f15ffbf8f4da/comments?cursor=43d35ef2-42f0-43be-b85c-f9614195bb39 +GET http://localhost:3000/articles/287cb4c8-48c5-49e1-82fa-a1b9e2d7b4e3/comments?cursor=4f9cd26c-09fa-441c-997f-2fdec0fb443e ### # 자유게시판 댓글 등록 @@ -57,13 +56,12 @@ POST http://localhost:3000/articles/287cb4c8-48c5-49e1-82fa-a1b9e2d7b4e3/comment Content-Type: application/json { - "content":"판다가 너무 귀여워요2", - "writer":"판다사랑나라사랑" + "content":"판다가 너무 귀여워요2" } ### # 자유게시판 댓글 수정 -PATCH http://localhost:3000/articles/2c027764-d7ef-4a94-8399-f15ffbf8f4da/comments/3c94cbe4-bd70-4c32-969a-225af0dd1e20 +PATCH http://localhost:3000/articles/287cb4c8-48c5-49e1-82fa-a1b9e2d7b4e3/comments/1a2b3c4d-5e6f-7g8h-9i0j-1k2l3m4n5o6p Content-Type: application/json { @@ -72,4 +70,4 @@ Content-Type: application/json ### # 자유게시판 댓글 삭제 -DELETE http://localhost:3000/articles/2c027764-d7ef-4a94-8399-f15ffbf8f4da/comments/3c94cbe4-bd70-4c32-969a-225af0dd1e20 \ No newline at end of file +DELETE http://localhost:3000/articles/287cb4c8-48c5-49e1-82fa-a1b9e2d7b4e3/comments/1a2b3c4d-5e6f-7g8h-9i0j-1k2l3m4n5o6p \ No newline at end of file diff --git a/http/products.http b/http/products.http index 36dcd96..1126069 100644 --- a/http/products.http +++ b/http/products.http @@ -13,7 +13,7 @@ GET http://localhost:3000/products?keyword=판다 ### # 상품 상세 조회 -GET http://localhost:3000/products/69c4bd5b-1281-4d6e-99de-fc18feed6de7 +GET http://localhost:3000/products/d4e8c9a0-5d45-4c9f-9b4b-7626f3c9c9a9 ### # 상품 등록 @@ -31,7 +31,7 @@ Content-Type: application/json ### # 상품 수정 -PATCH http://localhost:3000/products/69c4bd5b-1281-4d6e-99de-fc18feed6de7 +PATCH http://localhost:3000/products/d4e8c9a0-5d45-4c9f-9b4b-7626f3c9c9a9 Content-Type: application/json { @@ -43,15 +43,15 @@ Content-Type: application/json ### # 상품 삭제 -DELETE http://localhost:3000/products/69c4bd5b-1281-4d6e-99de-fc18feed6de7 +DELETE http://localhost:3000/products/377ce06c-23c8-46de-b86f-2cfd43d41cbc ### # 상품 좋아요 -PATCH http://localhost:3000/products/69c4bd5b-1281-4d6e-99de-fc18feed6de7/like +PATCH http://localhost:3000/products/377ce06c-23c8-46de-b86f-2cfd43d41cbc/like ### # 상품 좋아요 취소 -PATCH http://localhost:3000/products/69c4bd5b-1281-4d6e-99de-fc18feed6de7/unlike +PATCH http://localhost:3000/products/377ce06c-23c8-46de-b86f-2cfd43d41cbc/unlike ### # 상품 댓글 조회 @@ -59,7 +59,7 @@ GET http://localhost:3000/products/377ce06c-23c8-46de-b86f-2cfd43d41cbc/comments ### # 상품 댓글 커서 조회 -GET http://localhost:3000/products/377ce06c-23c8-46de-b86f-2cfd43d41cbc/comments?cursor=c45c56cf-d7d4-4592-885c-abcb56661bb5 +GET http://localhost:3000/products/377ce06c-23c8-46de-b86f-2cfd43d41cbc/comments?cursor=f81aac65-fb44-4973-8d6d-3789c6ba39d7 ### # 상품 댓글 등록 @@ -73,7 +73,7 @@ Content-Type: application/json ### # 상품 댓글 수정 -PATCH http://localhost:3000/products/377ce06c-23c8-46de-b86f-2cfd43d41cbc/comments/512b432b-7618-46ea-a500-579daacc0d02 +PATCH http://localhost:3000/products/377ce06c-23c8-46de-b86f-2cfd43d41cbc/comments/4db38f1c-53c5-40c4-98ce-bcaa0ba7904d Content-Type: application/json { @@ -82,4 +82,4 @@ Content-Type: application/json ### # 상품 댓글 삭제 -DELETE http://localhost:3000/products/377ce06c-23c8-46de-b86f-2cfd43d41cbc/comments/512b432b-7618-46ea-a500-579daacc0d02 \ No newline at end of file +DELETE http://localhost:3000/products/377ce06c-23c8-46de-b86f-2cfd43d41cbc/comments/4db38f1c-53c5-40c4-98ce-bcaa0ba7904d \ No newline at end of file diff --git a/prisma/migrations/20240523072342_change_writer_to_optional/migration.sql b/prisma/migrations/20240523072342_change_writer_to_optional/migration.sql new file mode 100644 index 0000000..c2d04be --- /dev/null +++ b/prisma/migrations/20240523072342_change_writer_to_optional/migration.sql @@ -0,0 +1,6 @@ +-- AlterTable +ALTER TABLE "Article" ALTER COLUMN "imageUrl" SET DEFAULT '', +ALTER COLUMN "writer" DROP NOT NULL; + +-- AlterTable +ALTER TABLE "Comment" ALTER COLUMN "writer" DROP NOT NULL; diff --git a/prisma/mock.js b/prisma/mock.js index 64ec75a..0b4e46e 100644 --- a/prisma/mock.js +++ b/prisma/mock.js @@ -127,4 +127,20 @@ export const COMMENTS = [ writer: "불곰인형 수집가", articleId: "287cb4c8-48c5-49e1-82fa-a1b9e2d7b4e3", }, + { + id: "3c4d5e6f-7g8h-9i0j-1k2l-3m4n5o6p7q8r", + content: "불곰인형 너무 귀여워요1", + writer: "불곰인형 수집가", + articleId: "287cb4c8-48c5-49e1-82fa-a1b9e2d7b4e3", + }, + { + content: "불곰인형 너무 귀여워요2", + writer: "불곰인형 수집가", + articleId: "287cb4c8-48c5-49e1-82fa-a1b9e2d7b4e3", + }, + { + content: "불곰인형 너무 귀여워요3", + writer: "불곰인형 수집가", + articleId: "287cb4c8-48c5-49e1-82fa-a1b9e2d7b4e3", + }, ]; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 4b24178..56b0da4 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -32,10 +32,10 @@ model Article { id String @id @default(uuid()) title String content String - imageUrl String? + imageUrl String? @default("") likeCount Int @default(0) isLiked Boolean @default(false) - writer String + writer String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt comments Comment[] @@ -44,7 +44,7 @@ model Article { model Comment { id String @id @default(uuid()) content String - writer String + writer String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt product Product? @relation(fields: [productId], references: [id], onDelete: SetNull) diff --git a/structs.js b/structs.js index 1050ee2..74e0e00 100644 --- a/structs.js +++ b/structs.js @@ -16,13 +16,15 @@ export const PatchProduct = s.partial(CreateProduct); export const CreateArticle = s.object({ title: s.string(), content: s.string(), + imageUrl: s.optional(s.string()), + writer: s.optional(s.string()), }); export const PatchArticle = s.partial(CreateArticle); export const CreateComment = s.object({ content: s.string(), - writer: s.string(), + writer: s.optional(s.string()), }); export const PatchComment = s.partial(CreateComment);