From 17a477da1fdb74db561a7dd70f0ea9980f864ab5 Mon Sep 17 00:00:00 2001 From: Chae Jeong Ah Date: Wed, 13 Dec 2023 00:04:16 +0900 Subject: [PATCH 01/22] =?UTF-8?q?[FIX]=20dev=20push=20server=20url=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=EC=97=90=20=EB=94=B0=EB=A5=B8=20env=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=EC=9A=A9=20=EB=B0=B0=ED=8F=AC=20(#322)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [DOCS]: README 다운 링크 추가 --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index eeeb9c6..78c8839 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,8 @@ # Havit-Server ### 기억하고 싶은 모든 콘텐츠를 내 손 안에, HAVIT + +[Playstore 에서 다운 받기](https://play.google.com/store/apps/details?id=org.sopt.havit&hl=ko&pli=1) +[Appstore 에서 다운 받기](https://apps.apple.com/kr/app/havit-%EC%BD%98%ED%85%90%EC%B8%A0-%EC%95%84%EC%B9%B4%EC%9D%B4%EB%B9%99-%EC%95%B1-%ED%95%B4%EB%B9%97/id1607518014) https://user-images.githubusercontent.com/55099365/150919289-52d35f31-c658-433a-8ffa-d84c8e6e85d8.mp4 ![해빗표지2](https://user-images.githubusercontent.com/20807197/150502331-7122ba4e-5544-496b-baac-0cee2a21edc5.png) From 00fe9fc9eab10f3829b4b478cb2bf004b5d315c8 Mon Sep 17 00:00:00 2001 From: Hyosik Philip Joo Date: Mon, 18 Dec 2023 00:14:04 -0800 Subject: [PATCH 02/22] =?UTF-8?q?[ADD]=20AWS=20cert=20=ED=8C=8C=EC=9D=BC?= =?UTF-8?q?=20ignore?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 +++ functions/.gitignore | 3 --- 2 files changed, 3 insertions(+), 3 deletions(-) delete mode 100644 functions/.gitignore diff --git a/.gitignore b/.gitignore index b4ccedf..20630ce 100644 --- a/.gitignore +++ b/.gitignore @@ -71,3 +71,6 @@ havit-production-firebase-adminsdk-bypl1-d081cc62e4.json # Deploy Message To Slack deployMessageToSlack.sh + +# AWS cert +havit-server-key.cer \ No newline at end of file diff --git a/functions/.gitignore b/functions/.gitignore deleted file mode 100644 index 49268ee..0000000 --- a/functions/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -node_modules/ -./package-lock.json -*.p8 \ No newline at end of file From 033f266d33edfdb8238de965a223d7a4f3bb7f5d Mon Sep 17 00:00:00 2001 From: Hyosik Philip Joo Date: Sun, 11 Feb 2024 23:19:01 -1000 Subject: [PATCH 03/22] =?UTF-8?q?[FEAT]=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20/?= =?UTF-8?q?=20=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85=20=EC=84=B1=EA=B3=B5=20?= =?UTF-8?q?=EC=8B=9C=20user=20id=20=EB=B0=98=ED=99=98=20(#323)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [FEAT] 로그인 성공 시 user id 반환 * [FEAT] 회원가입 성공 시 user id 반환 --- functions/api/routes/auth/signinPOST.js | 4 ++-- functions/api/routes/auth/signupPOST.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/functions/api/routes/auth/signinPOST.js b/functions/api/routes/auth/signinPOST.js index 54e5a16..7507aca 100644 --- a/functions/api/routes/auth/signinPOST.js +++ b/functions/api/routes/auth/signinPOST.js @@ -58,7 +58,7 @@ module.exports = asyncWrapper(async (req, res) => { await userDB.updateRefreshToken(dbConnection, kakaoUser.id, refreshToken); return res.status(statusCode.OK).send(util.success(statusCode.OK, responseMessage.SIGNIN_SUCCESS, - { firebaseAuthToken, accessToken, refreshToken, nickname })); + { firebaseAuthToken, accessToken, refreshToken, id: kakaoUser.id, nickname })); }; } else { @@ -90,7 +90,7 @@ module.exports = asyncWrapper(async (req, res) => { await userDB.updateRefreshToken(dbConnection, appleUser.id, refreshToken); return res.status(statusCode.OK).send(util.success(statusCode.OK, responseMessage.SIGNIN_SUCCESS, - { firebaseAuthToken, accessToken, refreshToken, nickname })); + { firebaseAuthToken, accessToken, refreshToken, id: appleUser.id, nickname })); }; } }); diff --git a/functions/api/routes/auth/signupPOST.js b/functions/api/routes/auth/signupPOST.js index ef7fdfa..f2d9071 100644 --- a/functions/api/routes/auth/signupPOST.js +++ b/functions/api/routes/auth/signupPOST.js @@ -38,7 +38,7 @@ module.exports = asyncWrapper(async (req, res) => { const refreshToken = jwtHandlers.signRefresh(); const kakaoUser = await userDB.addUser(dbConnection, firebaseUserId, nickname, email, age, gender, isOption, mongoId, refreshToken); const accessToken = jwtHandlers.sign({ id: kakaoUser.id, idFirebase: kakaoUser.idFirebase }); - return res.status(statusCode.CREATED).send(util.success(statusCode.CREATED, responseMessage.SIGNUP_SUCCESS, { firebaseAuthToken, accessToken, refreshToken, nickname })); + return res.status(statusCode.CREATED).send(util.success(statusCode.CREATED, responseMessage.SIGNUP_SUCCESS, { firebaseAuthToken, accessToken, refreshToken, id: kakaoUser.id, nickname })); } else { // kakaoAccessToken이 없을 때 (!kakaoAccessToken) : 애플 소셜 로그인 @@ -51,6 +51,6 @@ module.exports = asyncWrapper(async (req, res) => { const appleRefreshToken = await getAppleRefreshToken(appleCode); await userDB.updateAppleRefreshToken(dbConnection, appleUser.id, appleRefreshToken); - return res.status(statusCode.CREATED).send(util.success(statusCode.CREATED, responseMessage.SIGNUP_SUCCESS, { firebaseAuthToken, accessToken, refreshToken, nickname })); + return res.status(statusCode.CREATED).send(util.success(statusCode.CREATED, responseMessage.SIGNUP_SUCCESS, { firebaseAuthToken, accessToken, refreshToken, id: appleUser.id, nickname })); } }); From 3717a6e4228a310957945a2ed9cb97263d89e3ad Mon Sep 17 00:00:00 2001 From: Chae Jeong Ah Date: Sat, 2 Mar 2024 23:16:41 +0900 Subject: [PATCH 04/22] =?UTF-8?q?[DOCS]=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20README=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 78c8839..76ab244 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,9 @@ # Havit-Server ### 기억하고 싶은 모든 콘텐츠를 내 손 안에, HAVIT -[Playstore 에서 다운 받기](https://play.google.com/store/apps/details?id=org.sopt.havit&hl=ko&pli=1) -[Appstore 에서 다운 받기](https://apps.apple.com/kr/app/havit-%EC%BD%98%ED%85%90%EC%B8%A0-%EC%95%84%EC%B9%B4%EC%9D%B4%EB%B9%99-%EC%95%B1-%ED%95%B4%EB%B9%97/id1607518014) +[Playstore 에서 다운 받기](https://play.google.com/store/apps/details?id=org.sopt.havit&hl=ko&pli=1) +[Appstore 에서 다운 받기](https://apps.apple.com/kr/app/havit-%EC%BD%98%ED%85%90%EC%B8%A0-%EC%95%84%EC%B9%B4%EC%9D%B4%EB%B9%99-%EC%95%B1-%ED%95%B4%EB%B9%97/id1607518014) + https://user-images.githubusercontent.com/55099365/150919289-52d35f31-c658-433a-8ffa-d84c8e6e85d8.mp4 ![해빗표지2](https://user-images.githubusercontent.com/20807197/150502331-7122ba4e-5544-496b-baac-0cee2a21edc5.png) @@ -30,14 +31,8 @@ iOS의 Share Extension, Android의 Intent Filter를 사용하여 홈 화면으 ### 📋 IA ![image](https://user-images.githubusercontent.com/20807197/148189262-1dec5ee4-e543-4822-b930-e796ef405863.png) -### 💡 API 명세서 -[API 명세서](https://skitter-sloth-be4.notion.site/API-b7425add8a044c68b5aa86eaef17c571) - -### 📑 ERD - - ### ⚙️ Server Architecture -해빗앱서버아키텍쳐 +해빗앱서버아키텍쳐 ### 🛠 Development Environment From 449c4b709012ae853ad74ff2ab423de294bcb5dc Mon Sep 17 00:00:00 2001 From: Chae Jeong Ah Date: Tue, 12 Mar 2024 00:56:57 +0900 Subject: [PATCH 05/22] [FEAT] swagger setup (#327) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [DOCS]: .gitignore 내 swagger, auth key 추가 * [CHORE]: swagger package 추가 * [BUILD]: dev 파이프라인 내 swagger output 생성 * [FEAT] swagger-autogen, swagger-ui setup * [FEAT] swagger schema constants * [FEAT] swagger 명세 샘플 추가 * [DOCS]: .gitignore 내 swagger comment 추가 --- .github/workflows/dev.yml | 6 + .gitignore | 7 +- functions/api/index.js | 7 + functions/api/routes/index.js | 120 +++++++++++++++++- functions/api/routes/notice/index.js | 17 ++- functions/config/swagger.js | 42 ++++++ .../swagger/schemas/commonErrorSchema.js | 9 ++ functions/constants/swagger/schemas/index.js | 4 + .../constants/swagger/schemas/noticeSchema.js | 16 +++ functions/package-lock.json | 97 +++++++++++++- functions/package.json | 7 +- 11 files changed, 320 insertions(+), 12 deletions(-) create mode 100644 functions/config/swagger.js create mode 100644 functions/constants/swagger/schemas/commonErrorSchema.js create mode 100644 functions/constants/swagger/schemas/index.js create mode 100644 functions/constants/swagger/schemas/noticeSchema.js diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index a61b740..86a3196 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -53,6 +53,7 @@ jobs: echo "JWT_REFRESH_EXPIRE=${{ secrets.JWT_REFRESH_EXPIRE }} " >> .env.dev echo "SENTRY_DSN=${{ secrets.DEV_SENTRY_DSN }} " >> .env.dev echo "SENTRY_TRACES_SAMPLE_RATE=${{ secrets.DEV_SENTRY_TRACES_SAMPLE_RATE }} " >> .env.dev + echo "DEV_HOST=${{ secrets.DEV_HOST }} " >> .env.dev - name: create .p8 file run: | @@ -83,6 +84,11 @@ jobs: npm install -g firebase-tools npm install --save-dev cross-env + - name: Create swagger output file + run: | + cd functions + npm run swagger + - name: Deploy to Firebase run: | cd functions diff --git a/.gitignore b/.gitignore index 20630ce..340a329 100644 --- a/.gitignore +++ b/.gitignore @@ -73,4 +73,9 @@ havit-production-firebase-adminsdk-bypl1-d081cc62e4.json deployMessageToSlack.sh # AWS cert -havit-server-key.cer \ No newline at end of file +havit-server-key.cer + +AuthKey_* + +# Swagger +swagger-output.json \ No newline at end of file diff --git a/functions/api/index.js b/functions/api/index.js index 6e5df4c..da6d95f 100644 --- a/functions/api/index.js +++ b/functions/api/index.js @@ -7,6 +7,8 @@ const hpp = require("hpp"); const helmet = require("helmet"); const Sentry = require('@sentry/node'); const errorHandler = require('../middlewares/errorHandler'); +const swaggerUi = require("swagger-ui-express"); +const swaggerFile = require("../constants/swagger/swagger-output.json"); // initializing const app = express(); @@ -42,6 +44,11 @@ app.use(express.json()); app.use(express.urlencoded({extended: true})); app.use(cookieParser()); +if (process.env.NODE_ENV === "development") { + app.use("/swagger", swaggerUi.serve); + app.get("/swagger", swaggerUi.setup(swaggerFile)); +} + // 라우팅: routes 폴더로 정리 app.use("/", require("./routes")); app.use(Sentry.Handlers.errorHandler()); diff --git a/functions/api/routes/index.js b/functions/api/routes/index.js index 69e337b..55eb044 100644 --- a/functions/api/routes/index.js +++ b/functions/api/routes/index.js @@ -1,12 +1,118 @@ const express = require('express'); const router = express.Router(); -router.use('/content', require('./content')); -router.use('/category', require('./category')); -router.use('/recommendation', require('./recommendation')); -router.use('/user', require('./user')); -router.use('/auth', require('./auth')); -router.use('/notice', require('./notice')); -router.use('/health', require('./health')); +router.use( + '/content', require('./content') + /** + * #swagger.tags = ['content'] + * #swagger.responses[500] = { + description: "Internal Server Error", + content: { + "application/json": { + schema:{ + $ref: "#/components/schemas/internalServerErrorSchema" + } + } + } + } + */ +); +router.use( + '/category', require('./category') + /** + * #swagger.tags = ['category'] + * #swagger.responses[500] = { + description: "Internal Server Error", + content: { + "application/json": { + schema:{ + $ref: "#/components/schemas/internalServerErrorSchema" + } + } + } + } + */ +); +router.use( + '/recommendation', require('./recommendation') + /** + * #swagger.tags = ['recommendation'] + * #swagger.responses[500] = { + description: "Internal Server Error", + content: { + "application/json": { + schema:{ + $ref: "#/components/schemas/internalServerErrorSchema" + } + } + } + } + */ +); +router.use( + '/user', require('./user') + /** + * #swagger.tags = ['user'] + * #swagger.responses[500] = { + description: "Internal Server Error", + content: { + "application/json": { + schema:{ + $ref: "#/components/schemas/internalServerErrorSchema" + } + } + } + } + */ +); +router.use( + '/auth', require('./auth') + /** + * #swagger.tags = ['auth'] + * #swagger.responses[500] = { + description: "Internal Server Error", + content: { + "application/json": { + schema:{ + $ref: "#/components/schemas/internalServerErrorSchema" + } + } + } + } + */ +); +router.use( + '/notice', require('./notice') + /** + * #swagger.tags = ['notice'] + * #swagger.responses[500] = { + description: "Internal Server Error", + content: { + "application/json": { + schema:{ + $ref: "#/components/schemas/internalServerErrorSchema" + } + } + } + } + */ + +); +router.use( + '/health', require('./health') + /** + * #swagger.tags = ['health'] + * #swagger.responses[500] = { + description: "Internal Server Error", + content: { + "application/json": { + schema:{ + $ref: "#/components/schemas/internalServerErrorSchema" + } + } + } + } + */ +); module.exports = router; \ No newline at end of file diff --git a/functions/api/routes/notice/index.js b/functions/api/routes/notice/index.js index 02169a4..d633903 100644 --- a/functions/api/routes/notice/index.js +++ b/functions/api/routes/notice/index.js @@ -1,6 +1,21 @@ const express = require('express'); const router = express.Router(); -router.get('/', require('./noticeGET')); +router.get( + '/', require('./noticeGET') + /** + * #swagger.summary = "공지사항 전체 조회" + * #swagger.responses[200] = { + description: "공지사항 조회 성공", + content: { + "application/json": { + schema:{ + $ref: "#/components/schemas/responseNoticeSchema" + } + } + } + } + */ +); module.exports = router; \ No newline at end of file diff --git a/functions/config/swagger.js b/functions/config/swagger.js new file mode 100644 index 0000000..3e19775 --- /dev/null +++ b/functions/config/swagger.js @@ -0,0 +1,42 @@ +const swaggerAutogen = require('swagger-autogen')({ openapi: '3.0.0' }); +const { commonErrorSchema, noticeSchema } = require('../constants/swagger/schemas'); +const dotenv = require('dotenv'); +dotenv.config({ path: '.env.dev' }); + +const options = { + info: { + title: 'HAVIT API Docs', + description: 'HAVIT APP server API 문서입니다', + }, + host: "http://localhost:5001", + servers: [ + { + url: 'http://localhost:5001/havit-production/asia-northeast3/api', + description: '로컬 개발환경 host', + }, + { + url: process.env.DEV_HOST, + description: '개발환경 host', + }, + ], + schemes: ['http'], + securityDefinitions: { + bearerAuth: { + type: 'http', + name: 'x-auth-token', + in: 'header', + bearerFormat: 'JWT', + }, + }, + components: { + schemas: { + ...commonErrorSchema, + ...noticeSchema, + } + } +}; + +const outputFile = '../constants/swagger/swagger-output.json'; +const endpointsFiles = ['../api/index.js']; + +swaggerAutogen(outputFile, endpointsFiles, options); \ No newline at end of file diff --git a/functions/constants/swagger/schemas/commonErrorSchema.js b/functions/constants/swagger/schemas/commonErrorSchema.js new file mode 100644 index 0000000..2d356db --- /dev/null +++ b/functions/constants/swagger/schemas/commonErrorSchema.js @@ -0,0 +1,9 @@ +const internalServerErrorSchema = { + $status: 500, + $success: false, + $message: "서버 내부 오류", +}; + +module.exports = { + internalServerErrorSchema +} \ No newline at end of file diff --git a/functions/constants/swagger/schemas/index.js b/functions/constants/swagger/schemas/index.js new file mode 100644 index 0000000..75f8c6b --- /dev/null +++ b/functions/constants/swagger/schemas/index.js @@ -0,0 +1,4 @@ +module.exports = { + commonErrorSchema: require('./commonErrorSchema'), + noticeSchema: require('./noticeSchema'), +} \ No newline at end of file diff --git a/functions/constants/swagger/schemas/noticeSchema.js b/functions/constants/swagger/schemas/noticeSchema.js new file mode 100644 index 0000000..d83523e --- /dev/null +++ b/functions/constants/swagger/schemas/noticeSchema.js @@ -0,0 +1,16 @@ +const responseNoticeSchema = { + $status: 200, + $success: true, + $message: "공지사항 조회 성공", + $data: [ + { + $title: "notice title", + $url: "notice url", + $createdAt: "2022-09-15" + } + ] +}; + +module.exports = { + responseNoticeSchema +} \ No newline at end of file diff --git a/functions/package-lock.json b/functions/package-lock.json index f58a923..0371507 100644 --- a/functions/package-lock.json +++ b/functions/package-lock.json @@ -34,7 +34,9 @@ "eslint-config-google": "^0.14.0", "firebase-functions-test": "^0.2.0", "mocha": "^9.2.1", - "supertest": "^6.2.2" + "supertest": "^6.2.2", + "swagger-autogen": "^2.23.7", + "swagger-ui-express": "^5.0.0" }, "engines": { "node": "16" @@ -1603,6 +1605,15 @@ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/defer-to-connect": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", @@ -3173,6 +3184,18 @@ "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/jsonwebtoken": { "version": "8.5.1", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz", @@ -4800,6 +4823,39 @@ "node": ">=4" } }, + "node_modules/swagger-autogen": { + "version": "2.23.7", + "resolved": "https://registry.npmjs.org/swagger-autogen/-/swagger-autogen-2.23.7.tgz", + "integrity": "sha512-vr7uRmuV0DCxWc0wokLJAwX3GwQFJ0jwN+AWk0hKxre2EZwusnkGSGdVFd82u7fQLgwSTnbWkxUL7HXuz5LTZQ==", + "dev": true, + "dependencies": { + "acorn": "^7.4.1", + "deepmerge": "^4.2.2", + "glob": "^7.1.7", + "json5": "^2.2.3" + } + }, + "node_modules/swagger-ui-dist": { + "version": "5.11.8", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.11.8.tgz", + "integrity": "sha512-IfPtCPdf6opT5HXrzHO4kjL1eco0/8xJCtcs7ilhKuzatrpF2j9s+3QbOag6G3mVFKf+g+Ca5UG9DquVUs2obA==", + "dev": true + }, + "node_modules/swagger-ui-express": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.0.tgz", + "integrity": "sha512-tsU9tODVvhyfkNSvf03E6FAk+z+5cU3lXAzMy6Pv4av2Gt2xA0++fogwC4qo19XuFf6hdxevPuVCSKFuMHJhFA==", + "dev": true, + "dependencies": { + "swagger-ui-dist": ">=5.0.0" + }, + "engines": { + "node": ">= v0.10.32" + }, + "peerDependencies": { + "express": ">=4.0.0 || >=5.0.0-beta" + } + }, "node_modules/table": { "version": "6.7.5", "resolved": "https://registry.npmjs.org/table/-/table-6.7.5.tgz", @@ -6518,6 +6574,12 @@ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" }, + "deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true + }, "defer-to-connect": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", @@ -7701,6 +7763,12 @@ "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" }, + "json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true + }, "jsonwebtoken": { "version": "8.5.1", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz", @@ -8920,6 +8988,33 @@ "has-flag": "^3.0.0" } }, + "swagger-autogen": { + "version": "2.23.7", + "resolved": "https://registry.npmjs.org/swagger-autogen/-/swagger-autogen-2.23.7.tgz", + "integrity": "sha512-vr7uRmuV0DCxWc0wokLJAwX3GwQFJ0jwN+AWk0hKxre2EZwusnkGSGdVFd82u7fQLgwSTnbWkxUL7HXuz5LTZQ==", + "dev": true, + "requires": { + "acorn": "^7.4.1", + "deepmerge": "^4.2.2", + "glob": "^7.1.7", + "json5": "^2.2.3" + } + }, + "swagger-ui-dist": { + "version": "5.11.8", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.11.8.tgz", + "integrity": "sha512-IfPtCPdf6opT5HXrzHO4kjL1eco0/8xJCtcs7ilhKuzatrpF2j9s+3QbOag6G3mVFKf+g+Ca5UG9DquVUs2obA==", + "dev": true + }, + "swagger-ui-express": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.0.tgz", + "integrity": "sha512-tsU9tODVvhyfkNSvf03E6FAk+z+5cU3lXAzMy6Pv4av2Gt2xA0++fogwC4qo19XuFf6hdxevPuVCSKFuMHJhFA==", + "dev": true, + "requires": { + "swagger-ui-dist": ">=5.0.0" + } + }, "table": { "version": "6.7.5", "resolved": "https://registry.npmjs.org/table/-/table-6.7.5.tgz", diff --git a/functions/package.json b/functions/package.json index 6099093..f121583 100644 --- a/functions/package.json +++ b/functions/package.json @@ -9,7 +9,8 @@ "dev": "cross-env NODE_ENV=development firebase deploy --only functions,hosting", "deploy": "cross-env NODE_ENV=production firebase deploy --only functions,hosting", "logs": "firebase functions:log", - "test": "mocha" + "test": "mocha", + "swagger": "node config/swagger.js" }, "engines": { "node": "16" @@ -44,7 +45,9 @@ "eslint-config-google": "^0.14.0", "firebase-functions-test": "^0.2.0", "mocha": "^9.2.1", - "supertest": "^6.2.2" + "supertest": "^6.2.2", + "swagger-autogen": "^2.23.7", + "swagger-ui-express": "^5.0.0" }, "private": true } From cef8e291f46edd3a70d8ac0e7bdd940ec0c55d32 Mon Sep 17 00:00:00 2001 From: Chae Jeong Ah Date: Tue, 12 Mar 2024 03:03:15 +0900 Subject: [PATCH 06/22] =?UTF-8?q?[FIX]=20swagger-ui=20package=20=EC=A2=85?= =?UTF-8?q?=EC=86=8D=EC=84=B1=20=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0=20?= =?UTF-8?q?(#330)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/dev.yml | 2 +- functions/package-lock.json | 26 ++++++++------------------ functions/package.json | 8 ++++---- 3 files changed, 13 insertions(+), 23 deletions(-) diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index 86a3196..ef58127 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -80,7 +80,7 @@ jobs: - name: Install npm pacakges run: | cd functions - npm install + npm ci npm install -g firebase-tools npm install --save-dev cross-env diff --git a/functions/package-lock.json b/functions/package-lock.json index 0371507..966a4e9 100644 --- a/functions/package-lock.json +++ b/functions/package-lock.json @@ -26,7 +26,9 @@ "open-graph-scraper": "^4.11.0", "pg": "^8.7.1", "request": "^2.88.2", - "request-promise": "^4.2.6" + "request-promise": "^4.2.6", + "swagger-autogen": "^2.23.7", + "swagger-ui-express": "^5.0.0" }, "devDependencies": { "chai": "^4.3.4", @@ -34,9 +36,7 @@ "eslint-config-google": "^0.14.0", "firebase-functions-test": "^0.2.0", "mocha": "^9.2.1", - "supertest": "^6.2.2", - "swagger-autogen": "^2.23.7", - "swagger-ui-express": "^5.0.0" + "supertest": "^6.2.2" }, "engines": { "node": "16" @@ -1609,7 +1609,6 @@ "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -3188,7 +3187,6 @@ "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, "bin": { "json5": "lib/cli.js" }, @@ -4827,7 +4825,6 @@ "version": "2.23.7", "resolved": "https://registry.npmjs.org/swagger-autogen/-/swagger-autogen-2.23.7.tgz", "integrity": "sha512-vr7uRmuV0DCxWc0wokLJAwX3GwQFJ0jwN+AWk0hKxre2EZwusnkGSGdVFd82u7fQLgwSTnbWkxUL7HXuz5LTZQ==", - "dev": true, "dependencies": { "acorn": "^7.4.1", "deepmerge": "^4.2.2", @@ -4838,14 +4835,12 @@ "node_modules/swagger-ui-dist": { "version": "5.11.8", "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.11.8.tgz", - "integrity": "sha512-IfPtCPdf6opT5HXrzHO4kjL1eco0/8xJCtcs7ilhKuzatrpF2j9s+3QbOag6G3mVFKf+g+Ca5UG9DquVUs2obA==", - "dev": true + "integrity": "sha512-IfPtCPdf6opT5HXrzHO4kjL1eco0/8xJCtcs7ilhKuzatrpF2j9s+3QbOag6G3mVFKf+g+Ca5UG9DquVUs2obA==" }, "node_modules/swagger-ui-express": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.0.tgz", "integrity": "sha512-tsU9tODVvhyfkNSvf03E6FAk+z+5cU3lXAzMy6Pv4av2Gt2xA0++fogwC4qo19XuFf6hdxevPuVCSKFuMHJhFA==", - "dev": true, "dependencies": { "swagger-ui-dist": ">=5.0.0" }, @@ -6577,8 +6572,7 @@ "deepmerge": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "dev": true + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==" }, "defer-to-connect": { "version": "2.0.1", @@ -7766,8 +7760,7 @@ "json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==" }, "jsonwebtoken": { "version": "8.5.1", @@ -8992,7 +8985,6 @@ "version": "2.23.7", "resolved": "https://registry.npmjs.org/swagger-autogen/-/swagger-autogen-2.23.7.tgz", "integrity": "sha512-vr7uRmuV0DCxWc0wokLJAwX3GwQFJ0jwN+AWk0hKxre2EZwusnkGSGdVFd82u7fQLgwSTnbWkxUL7HXuz5LTZQ==", - "dev": true, "requires": { "acorn": "^7.4.1", "deepmerge": "^4.2.2", @@ -9003,14 +8995,12 @@ "swagger-ui-dist": { "version": "5.11.8", "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.11.8.tgz", - "integrity": "sha512-IfPtCPdf6opT5HXrzHO4kjL1eco0/8xJCtcs7ilhKuzatrpF2j9s+3QbOag6G3mVFKf+g+Ca5UG9DquVUs2obA==", - "dev": true + "integrity": "sha512-IfPtCPdf6opT5HXrzHO4kjL1eco0/8xJCtcs7ilhKuzatrpF2j9s+3QbOag6G3mVFKf+g+Ca5UG9DquVUs2obA==" }, "swagger-ui-express": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.0.tgz", "integrity": "sha512-tsU9tODVvhyfkNSvf03E6FAk+z+5cU3lXAzMy6Pv4av2Gt2xA0++fogwC4qo19XuFf6hdxevPuVCSKFuMHJhFA==", - "dev": true, "requires": { "swagger-ui-dist": ">=5.0.0" } diff --git a/functions/package.json b/functions/package.json index f121583..4bd5dc3 100644 --- a/functions/package.json +++ b/functions/package.json @@ -37,7 +37,9 @@ "open-graph-scraper": "^4.11.0", "pg": "^8.7.1", "request": "^2.88.2", - "request-promise": "^4.2.6" + "request-promise": "^4.2.6", + "swagger-autogen": "^2.23.7", + "swagger-ui-express": "^5.0.0" }, "devDependencies": { "chai": "^4.3.4", @@ -45,9 +47,7 @@ "eslint-config-google": "^0.14.0", "firebase-functions-test": "^0.2.0", "mocha": "^9.2.1", - "supertest": "^6.2.2", - "swagger-autogen": "^2.23.7", - "swagger-ui-express": "^5.0.0" + "supertest": "^6.2.2" }, "private": true } From 30f21bae956af5bc5dd06c2dadfdbb16d8bc5e32 Mon Sep 17 00:00:00 2001 From: Chae Jeong Ah Date: Wed, 13 Mar 2024 19:59:35 +0900 Subject: [PATCH 07/22] =?UTF-8?q?[FEAT]=20=EC=BB=A4=EB=AE=A4=EB=8B=88?= =?UTF-8?q?=ED=8B=B0=20=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC,=20=EA=B2=8C?= =?UTF-8?q?=EC=8B=9C=EA=B8=80=20=EC=83=81=EC=84=B8=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?swagger=20=EB=AA=85=EC=84=B8=20(#331)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [FEAT] community swagger schema 추가 * [FEAT] community category, post 조회 swagger 명세 * [DOCS]: .gitignore 내 .vscode 추가 --- .gitignore | 5 ++- functions/api/index.js | 20 +++++----- .../routes/community/communityCategoryGET.js | 9 +++++ .../api/routes/community/communityPostGET.js | 9 +++++ functions/api/routes/community/index.js | 39 +++++++++++++++++++ functions/api/routes/index.js | 23 +++++++++-- functions/config/swagger.js | 3 +- .../swagger/schemas/communitySchema.js | 34 ++++++++++++++++ functions/constants/swagger/schemas/index.js | 1 + 9 files changed, 128 insertions(+), 15 deletions(-) create mode 100644 functions/api/routes/community/communityCategoryGET.js create mode 100644 functions/api/routes/community/communityPostGET.js create mode 100644 functions/api/routes/community/index.js create mode 100644 functions/constants/swagger/schemas/communitySchema.js diff --git a/.gitignore b/.gitignore index 340a329..51710e5 100644 --- a/.gitignore +++ b/.gitignore @@ -78,4 +78,7 @@ havit-server-key.cer AuthKey_* # Swagger -swagger-output.json \ No newline at end of file +swagger-output.json + +#VSCode +.vscode \ No newline at end of file diff --git a/functions/api/index.js b/functions/api/index.js index da6d95f..582184f 100644 --- a/functions/api/index.js +++ b/functions/api/index.js @@ -14,14 +14,14 @@ const swaggerFile = require("../constants/swagger/swagger-output.json"); const app = express(); Sentry.init({ - dsn: process.env.SENTRY_DSN, - environment: `${process.env.NODE_ENV}_app`, - integrations: [ - new Sentry.Integrations.Http({ tracing: true }), - new Sentry.Integrations.Express({ app }), - ...Sentry.autoDiscoverNodePerformanceMonitoringIntegrations(), - ], - tracesSampleRate: process.env.SENTRY_TRACES_SAMPLE_RATE, + dsn: process.env.SENTRY_DSN, + environment: `${process.env.NODE_ENV}_app`, + integrations: [ + new Sentry.Integrations.Http({ tracing: true }), + new Sentry.Integrations.Express({ app }), + ...Sentry.autoDiscoverNodePerformanceMonitoringIntegrations(), + ], + tracesSampleRate: process.env.SENTRY_TRACES_SAMPLE_RATE, }); app.use(Sentry.Handlers.requestHandler()); @@ -41,7 +41,7 @@ if (process.env.NODE_ENV === "production") { // request에 담긴 정보를 json 형태로 파싱하기 위한 미들웨어들 app.use(express.json()); -app.use(express.urlencoded({extended: true})); +app.use(express.urlencoded({ extended: true })); app.use(cookieParser()); if (process.env.NODE_ENV === "development") { @@ -72,7 +72,7 @@ module.exports = functions }) .region("asia-northeast3") // 서버가 돌아갈 region. asia-northeast3는 서울 .https.onRequest(async (req, res) => { - + // 들어오는 요청에 대한 로그를 콘솔에 찍기. 디버깅 때 유용하게 쓰일 예정. // 콘솔에 찍고 싶은 내용을 원하는 대로 추가하면 됨. (req.headers, req.query 등) console.log("\n\n", "[api]", `[${req.method.toUpperCase()}]`, req.originalUrl, req.body); diff --git a/functions/api/routes/community/communityCategoryGET.js b/functions/api/routes/community/communityCategoryGET.js new file mode 100644 index 0000000..455b688 --- /dev/null +++ b/functions/api/routes/community/communityCategoryGET.js @@ -0,0 +1,9 @@ +/** + * @route GET /community/category + * @desc 커뮤니티 카테고리 조회 + * @access Public + */ + +module.exports = async (req, res) => { + +}; \ No newline at end of file diff --git a/functions/api/routes/community/communityPostGET.js b/functions/api/routes/community/communityPostGET.js new file mode 100644 index 0000000..3c2df1f --- /dev/null +++ b/functions/api/routes/community/communityPostGET.js @@ -0,0 +1,9 @@ +/** + * @route GET /community/posts/:communityPostId + * @desc 커뮤니티 게시글 상세 조회 + * @access Private + */ + +module.exports = async (req, res) => { + +}; \ No newline at end of file diff --git a/functions/api/routes/community/index.js b/functions/api/routes/community/index.js new file mode 100644 index 0000000..355fa89 --- /dev/null +++ b/functions/api/routes/community/index.js @@ -0,0 +1,39 @@ +const express = require('express'); +const router = express.Router(); +const { checkUser } = require('../../../middlewares/auth'); + +router.get('/category', require('./communityCategoryGET') + /** + * #swagger.summary = "커뮤니티 카테고리 전체 조회" + * #swagger.responses[200] = { + description: "커뮤니티 카테고리 조회 성공", + content: { + "application/json": { + schema:{ + $ref: "#/components/schemas/responseCommunityCategorySchema" + } + } + } + } + */ +); + +router.get('/posts/:communityPostId', checkUser, require('./communityPostGET') + /** + * #swagger.summary = "커뮤니티 게시글 상세 조회" + * #swagger.responses[200] = { + description: "커뮤니티 게시글 상세 조회 성공", + content: { + "application/json": { + schema:{ + $ref: "#/components/schemas/responseCommunityPostsDetailSchema" + } + } + } + } + * #swagger.responses[400] + * #swagger.responses[404] + */ +); + +module.exports = router; \ No newline at end of file diff --git a/functions/api/routes/index.js b/functions/api/routes/index.js index 55eb044..9f012b7 100644 --- a/functions/api/routes/index.js +++ b/functions/api/routes/index.js @@ -2,7 +2,7 @@ const express = require('express'); const router = express.Router(); router.use( - '/content', require('./content') + '/content', require('./content') /** * #swagger.tags = ['content'] * #swagger.responses[500] = { @@ -15,7 +15,7 @@ router.use( } } } - */ + */ ); router.use( '/category', require('./category') @@ -96,7 +96,7 @@ router.use( } } */ - + ); router.use( '/health', require('./health') @@ -115,4 +115,21 @@ router.use( */ ); +router.use( + '/community', require('./community') + /** + * #swagger.tags = ['community'] + * #swagger.responses[500] = { + description: "Internal Server Error", + content: { + "application/json": { + schema:{ + $ref: "#/components/schemas/internalServerErrorSchema" + } + } + } + } + */ +); + module.exports = router; \ No newline at end of file diff --git a/functions/config/swagger.js b/functions/config/swagger.js index 3e19775..6e070e6 100644 --- a/functions/config/swagger.js +++ b/functions/config/swagger.js @@ -1,5 +1,5 @@ const swaggerAutogen = require('swagger-autogen')({ openapi: '3.0.0' }); -const { commonErrorSchema, noticeSchema } = require('../constants/swagger/schemas'); +const { commonErrorSchema, noticeSchema, communitySchema } = require('../constants/swagger/schemas'); const dotenv = require('dotenv'); dotenv.config({ path: '.env.dev' }); @@ -32,6 +32,7 @@ const options = { schemas: { ...commonErrorSchema, ...noticeSchema, + ...communitySchema, } } }; diff --git a/functions/constants/swagger/schemas/communitySchema.js b/functions/constants/swagger/schemas/communitySchema.js new file mode 100644 index 0000000..409e8de --- /dev/null +++ b/functions/constants/swagger/schemas/communitySchema.js @@ -0,0 +1,34 @@ +const responseCommunityCategorySchema = { + $status: 200, + $success: true, + $message: "커뮤니티 카테고리 조회 성공", + $data: [ + { + $id: 1, + $name: "UI/UX" + }, + ] +}; + +const responseCommunityPostsDetailSchema = { + $status: 200, + $success: true, + $message: "커뮤니티 게시글 상세 조회 성공", + $data: { + $id: 1, + $nickname: "잡채", + $profileImage: "https://s3~", + $title: "제목", + $body: "본문", + $contentUrl: "https://naver.com", + $contentTitle: "콘텐츠 링크 제목", + $contentDescription: "콘텐츠 링크 설명", + $thumbnailUrl: "https://content-thumbnail-image-url", + $createdAt: "2024-02-01", + }, +} + +module.exports = { + responseCommunityCategorySchema, + responseCommunityPostsDetailSchema, +}; \ No newline at end of file diff --git a/functions/constants/swagger/schemas/index.js b/functions/constants/swagger/schemas/index.js index 75f8c6b..8c1c793 100644 --- a/functions/constants/swagger/schemas/index.js +++ b/functions/constants/swagger/schemas/index.js @@ -1,4 +1,5 @@ module.exports = { commonErrorSchema: require('./commonErrorSchema'), noticeSchema: require('./noticeSchema'), + communitySchema: require('./communitySchema'), } \ No newline at end of file From b552c5163cd1af2842b7118196e98ced2f88d913 Mon Sep 17 00:00:00 2001 From: yubinquitous <65652094+yubinquitous@users.noreply.github.com> Date: Sun, 17 Mar 2024 19:34:23 +0900 Subject: [PATCH 08/22] =?UTF-8?q?[FEAT]=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20?= =?UTF-8?q?=EC=A0=84=EC=B2=B4=20=EC=A1=B0=ED=9A=8C,=20=EC=8B=A0=EA=B3=A0?= =?UTF-8?q?=ED=95=98=EA=B8=B0=20swagger=20=EB=AA=85=EC=84=B8=20(#333)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [FEAT] community post 전체 조회 명세 * [FEAT] community 신고 swagger 명세 * [CHORE] @route에서 ?page삭제 Co-authored-by: Hyosik Philip Joo * [CHORE] router GET /posts의communityPostListGet을 communityPostListGET으로 변경 * Rename [CHORE] communityPostCategoryGet.js to communityPostCategoryGET.js * [CHORE] Rename communityPostListGet.js to communityPostListGET.js * [CHORE] responseCommunityPostsListSchema->responseCommunityPostsSchema로 변경, POST report swagger.requestBody 추가 * [CHORE] 게시글 전체 조회, 카테고리 조회 request query에 limit 추가, response에 totalItemCount 추가 * [CHORE] 커뮤니티 카테고리별 게시글 조회 /community/category/:communityCategoryId 로 엔드포인트 변경 * [CHORE] 커뮤니티 게시물 신고 api에 404 에러 명세 추가 --------- Co-authored-by: Hyosik Philip Joo --- functions/.pretterrc.js | 9 -- functions/.prettierrc.js | 9 ++ .../community/communityCategoryPostsGET.js | 9 ++ .../api/routes/community/communityPostsGET.js | 9 ++ .../routes/community/communityReportPOST.js | 9 ++ functions/api/routes/community/index.js | 118 +++++++++++++++++- .../swagger/schemas/communitySchema.js | 114 +++++++++++++---- 7 files changed, 236 insertions(+), 41 deletions(-) delete mode 100644 functions/.pretterrc.js create mode 100644 functions/.prettierrc.js create mode 100644 functions/api/routes/community/communityCategoryPostsGET.js create mode 100644 functions/api/routes/community/communityPostsGET.js create mode 100644 functions/api/routes/community/communityReportPOST.js diff --git a/functions/.pretterrc.js b/functions/.pretterrc.js deleted file mode 100644 index da58a24..0000000 --- a/functions/.pretterrc.js +++ /dev/null @@ -1,9 +0,0 @@ -module.exports = { - bracketSpacing: true, - jsxBracketSameLine: true, - singleQuote: true, - trailingComma: "all", - arrowParens: "always", - printWidth: 200, - tabWidth: 2, -}; \ No newline at end of file diff --git a/functions/.prettierrc.js b/functions/.prettierrc.js new file mode 100644 index 0000000..d4d99f2 --- /dev/null +++ b/functions/.prettierrc.js @@ -0,0 +1,9 @@ +module.exports = { + bracketSpacing: true, + jsxBracketSameLine: true, + singleQuote: true, + trailingComma: 'all', + arrowParens: 'always', + printWidth: 200, + tabWidth: 2, +}; diff --git a/functions/api/routes/community/communityCategoryPostsGET.js b/functions/api/routes/community/communityCategoryPostsGET.js new file mode 100644 index 0000000..0248689 --- /dev/null +++ b/functions/api/routes/community/communityCategoryPostsGET.js @@ -0,0 +1,9 @@ +/** + * @route GET /community/category/:communityCategoryId + * @desc 커뮤니티 카테고리별 게시글 조회 + * @access Private + */ + +module.exports = async (req, res) => { + const { page, limit } = req.query; +}; diff --git a/functions/api/routes/community/communityPostsGET.js b/functions/api/routes/community/communityPostsGET.js new file mode 100644 index 0000000..e486bfe --- /dev/null +++ b/functions/api/routes/community/communityPostsGET.js @@ -0,0 +1,9 @@ +/** + * @route GET /community/posts + * @desc 커뮤니티 게시글 전체 조회 + * @access Private + */ + +module.exports = async (req, res) => { + const { page, limit } = req.query; +}; diff --git a/functions/api/routes/community/communityReportPOST.js b/functions/api/routes/community/communityReportPOST.js new file mode 100644 index 0000000..6e7abf6 --- /dev/null +++ b/functions/api/routes/community/communityReportPOST.js @@ -0,0 +1,9 @@ +/** + * @route POST /community/report + * @desc 커뮤니티 게시글 신고 + * @access Private + */ + +module.exports = async (req, res) => { + const { communityPostId } = req.body; +}; diff --git a/functions/api/routes/community/index.js b/functions/api/routes/community/index.js index 355fa89..2b0b439 100644 --- a/functions/api/routes/community/index.js +++ b/functions/api/routes/community/index.js @@ -2,8 +2,10 @@ const express = require('express'); const router = express.Router(); const { checkUser } = require('../../../middlewares/auth'); -router.get('/category', require('./communityCategoryGET') - /** +router.get( + '/category', + require('./communityCategoryGET'), + /** * #swagger.summary = "커뮤니티 카테고리 전체 조회" * #swagger.responses[200] = { description: "커뮤니티 카테고리 조회 성공", @@ -18,9 +20,51 @@ router.get('/category', require('./communityCategoryGET') */ ); -router.get('/posts/:communityPostId', checkUser, require('./communityPostGET') - /** +router.get( + '/category/:communityCategoryId', + checkUser, + require('./communityCategoryPostsGET'), + /** + * #swagger.summary = "커뮤니티 카테고리별 게시글 조회" + * #swagger.parameters['page'] = { + in: 'query', + description: '페이지 번호', + type: 'number', + required: true + } + * #swagger.parameters['limit'] = { + in: 'query', + description: '페이지 당 게시글 수', + type: 'number', + required: true + } + * #swagger.responses[200] = { + description: "커뮤니티 게시글 카테고리별 조회 성공", + content: { + "application/json": { + schema:{ + $ref: "#/components/schemas/responseCommunityPostsSchema" + } + } + } + } + * #swagger.responses[400] + * #swagger.responses[404] + */ +); + +router.get( + '/posts/:communityPostId', + checkUser, + require('./communityPostGET'), + /** * #swagger.summary = "커뮤니티 게시글 상세 조회" + * #swagger.parameters['communityPostId'] = { + in: 'path', + description: '커뮤니티 게시글 아이디', + type: 'number', + required: true + } * #swagger.responses[200] = { description: "커뮤니티 게시글 상세 조회 성공", content: { @@ -36,4 +80,68 @@ router.get('/posts/:communityPostId', checkUser, require('./communityPostGET') */ ); -module.exports = router; \ No newline at end of file +router.get( + '/posts', + checkUser, + require('./communityPostsGET'), + /** + * #swagger.summary = "커뮤니티 게시글 전체 조회" + * #swagger.parameters['page'] = { + in: 'query', + description: '페이지 번호', + type: 'number', + required: true + } + * #swagger.parameters['limit'] = { + in: 'query', + description: '페이지 당 게시글 수', + type: 'number', + required: true + } + * #swagger.responses[200] = { + description: "커뮤니티 게시글 전체 조회 성공", + content: { + "application/json": { + schema:{ + $ref: "#/components/schemas/responseCommunityPostsSchema" + } + } + } + } + * #swagger.responses[400] + * #swagger.responses[404] + */ +); + +router.post( + '/report', + checkUser, + require('./communityReportPOST'), + /** + * #swagger.summary = "커뮤니티 게시글 신고" + * #swagger.requestBody = { + required: true, + content: { + "application/json": { + schema:{ + $ref: "#/components/schemas/requestCommunityReportSchema" + } + } + } + } + * #swagger.responses[201] = { + description: "커뮤니티 게시글 신고 성공", + content: { + "application/json": { + schema:{ + $ref: "#/components/schemas/responseCommunityReportSchema" + } + } + } + } + * #swagger.responses[400] + * #swagger.responses[404] + */ +); + +module.exports = router; diff --git a/functions/constants/swagger/schemas/communitySchema.js b/functions/constants/swagger/schemas/communitySchema.js index 409e8de..439e44e 100644 --- a/functions/constants/swagger/schemas/communitySchema.js +++ b/functions/constants/swagger/schemas/communitySchema.js @@ -1,34 +1,94 @@ const responseCommunityCategorySchema = { - $status: 200, - $success: true, - $message: "커뮤니티 카테고리 조회 성공", - $data: [ - { - $id: 1, - $name: "UI/UX" - }, - ] + $status: 200, + $success: true, + $message: '커뮤니티 카테고리 조회 성공', + $data: [ + { + $id: 1, + $name: 'UI/UX', + }, + ], }; const responseCommunityPostsDetailSchema = { - $status: 200, - $success: true, - $message: "커뮤니티 게시글 상세 조회 성공", - $data: { + $status: 200, + $success: true, + $message: '커뮤니티 게시글 상세 조회 성공', + $data: { + $id: 1, + $nickname: '잡채', + $profileImage: 'https://s3~', + $title: '제목', + $body: '본문', + $contentUrl: 'https://naver.com', + $contentTitle: '콘텐츠 링크 제목', + $contentDescription: '콘텐츠 링크 설명', + $thumbnailUrl: 'https://content-thumbnail-image-url', + $createdAt: '2024-02-01', + }, +}; + +const responseCommunityPostsSchema = { + $status: 200, + $success: true, + $message: '커뮤니티 게시글 전체 조회 성공', + $data: { + $posts: [ + { $id: 1, - $nickname: "잡채", - $profileImage: "https://s3~", - $title: "제목", - $body: "본문", - $contentUrl: "https://naver.com", - $contentTitle: "콘텐츠 링크 제목", - $contentDescription: "콘텐츠 링크 설명", - $thumbnailUrl: "https://content-thumbnail-image-url", - $createdAt: "2024-02-01", - }, -} + $nickname: '잡채', + $profileImage: 'https://s3~', + $title: '제목1', + $body: '본문1', + $contentUrl: 'https://naver.com', + $contentTitle: '콘텐츠 링크 제목1', + $contentDescription: '콘텐츠 링크 설명1', + $thumbnailUrl: 'https://content-thumbnail-image-url1', + $createdAt: '2024-02-01', + }, + { + $id: 2, + $nickname: '필립', + $profileImage: 'https://s3~', + $title: '제목2', + $body: '본문2', + $contentUrl: 'https://example2.com', + $contentTitle: '콘텐츠 링크 제목2', + $contentDescription: '콘텐츠 링크 설명2', + $thumbnailUrl: 'https://content-thumbnail-image-url2', + $createdAt: '2024-02-02', + }, + { + $id: 3, + $nickname: '윱최', + $profileImage: 'https://s3~', + $title: '제목3', + $body: '본문3', + $contentUrl: 'https://example3.com', + $contentTitle: '콘텐츠 링크 제목3', + $contentDescription: '콘텐츠 링크 설명3', + $thumbnailUrl: 'https://content-thumbnail-image-url3', + $createdAt: '2024-02-03', + }, + ], + $totalItemCount: 3, + }, +}; + +const responseCommunityReportSchema = { + $status: 200, + $success: true, + $message: '커뮤니티 게시글 신고 성공', +}; + +const requestCommunityReportSchema = { + $communityPostId: 1, +}; module.exports = { - responseCommunityCategorySchema, - responseCommunityPostsDetailSchema, -}; \ No newline at end of file + responseCommunityCategorySchema, + responseCommunityPostsDetailSchema, + responseCommunityPostsSchema, + responseCommunityReportSchema, + requestCommunityReportSchema, +}; From 6e1ea7b6d5f25349f7abb2f2832baa81df22f975 Mon Sep 17 00:00:00 2001 From: Chae Jeong Ah Date: Wed, 20 Mar 2024 23:30:24 +0900 Subject: [PATCH 09/22] =?UTF-8?q?[FEAT]=20community=20=EA=B2=8C=EC=8B=9C?= =?UTF-8?q?=EA=B8=80=20=EC=9E=91=EC=84=B1=20swagger=20=EB=AA=85=EC=84=B8?= =?UTF-8?q?=20(#335)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [FEAT] community 게시글 작성 swagger 명세 * [FIX] communityPost 파일명 수정 --- .../api/routes/community/communityPostPOST.js | 9 ++++++ functions/api/routes/community/index.js | 30 +++++++++++++++++++ .../swagger/schemas/communitySchema.js | 20 ++++++++++++- 3 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 functions/api/routes/community/communityPostPOST.js diff --git a/functions/api/routes/community/communityPostPOST.js b/functions/api/routes/community/communityPostPOST.js new file mode 100644 index 0000000..948699e --- /dev/null +++ b/functions/api/routes/community/communityPostPOST.js @@ -0,0 +1,9 @@ +/** + * @route POST /community/posts + * @desc 커뮤니티 게시글 작성 + * @access Private + */ + +module.exports = async (req, res) => { + const { communityPostId } = req.body; +}; diff --git a/functions/api/routes/community/index.js b/functions/api/routes/community/index.js index 2b0b439..60507cc 100644 --- a/functions/api/routes/community/index.js +++ b/functions/api/routes/community/index.js @@ -113,6 +113,36 @@ router.get( */ ); +router.post( + '/posts', + checkUser, + require('./communityPostPOST'), + /** + * #swagger.summary = "커뮤니티 글 작성" + * #swagger.requestBody = { + required: true, + content: { + "application/json": { + schema:{ + $ref: "#/components/schemas/requestCreateCommunityPostSchema" + } + } + } + } + * #swagger.responses[201] = { + description: "커뮤니티 게시글 작성 성공", + content: { + "application/json": { + schema:{ + $ref: "#/components/schemas/responseCreateCommunityPostSchema" + } + } + } + } + * #swagger.responses[400] + */ +); + router.post( '/report', checkUser, diff --git a/functions/constants/swagger/schemas/communitySchema.js b/functions/constants/swagger/schemas/communitySchema.js index 439e44e..6e45dff 100644 --- a/functions/constants/swagger/schemas/communitySchema.js +++ b/functions/constants/swagger/schemas/communitySchema.js @@ -75,8 +75,24 @@ const responseCommunityPostsSchema = { }, }; +const responseCreateCommunityPostSchema = { + $status: 201, + $success: true, + $message: '커뮤니티 게시글 작성 성공', +}; + +const requestCreateCommunityPostSchema = { + $communityCategoryIds: [1, 2, 3], + $title: '게시글 제목', + $body: '게시글 본문', + $contentUrl: '공유하는 콘텐츠 링크', + $contentTitle: '공유하는 콘텐츠 제목(og:title)', + contentDescription: '공유하는 콘텐츠 description(og:description)', + thumbnailUrl: '공유하는 콘텐츠 thumbnail(og:image)', +}; + const responseCommunityReportSchema = { - $status: 200, + $status: 201, $success: true, $message: '커뮤니티 게시글 신고 성공', }; @@ -89,6 +105,8 @@ module.exports = { responseCommunityCategorySchema, responseCommunityPostsDetailSchema, responseCommunityPostsSchema, + responseCreateCommunityPostSchema, + requestCreateCommunityPostSchema, responseCommunityReportSchema, requestCommunityReportSchema, }; From d85e26fa6cd31e38191125f14dbecda9601c1bfc Mon Sep 17 00:00:00 2001 From: Chae Jeong Ah Date: Mon, 25 Mar 2024 22:37:28 +0900 Subject: [PATCH 10/22] =?UTF-8?q?[FEAT]=20=EC=BB=A4=EB=AE=A4=EB=8B=88?= =?UTF-8?q?=ED=8B=B0=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20=EC=83=81=EC=84=B8?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20API=20(#339)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [FEAT] user profile dummy image url 추가 * [FEAT] 커뮤니티 게시글 상세조회 API * [FIX] createdAt 형식 포맷팅 * [FIX] db query 결과 반환 형식 수정 * [FIX] createdAt format 수정 * [FIX] createdAt swagger schema 수정 --- .../api/routes/community/communityPostGET.js | 37 +++++- functions/constants/dummyImages.js | 5 +- functions/constants/responseMessage.js | 124 +++++++++--------- .../swagger/schemas/communitySchema.js | 8 +- functions/db/community.js | 17 +++ functions/db/index.js | 15 ++- 6 files changed, 131 insertions(+), 75 deletions(-) create mode 100644 functions/db/community.js diff --git a/functions/api/routes/community/communityPostGET.js b/functions/api/routes/community/communityPostGET.js index 3c2df1f..02ab97d 100644 --- a/functions/api/routes/community/communityPostGET.js +++ b/functions/api/routes/community/communityPostGET.js @@ -1,9 +1,42 @@ +const dayjs = require('dayjs'); +const customParseFormat = require('dayjs/plugin/customParseFormat'); +const util = require('../../../lib/util'); +const statusCode = require('../../../constants/statusCode'); +const responseMessage = require('../../../constants/responseMessage'); +const dummyImages = require('../../../constants/dummyImages'); +const db = require('../../../db/db'); +const { communityDB } = require('../../../db'); +const asyncWrapper = require('../../../lib/asyncWrapper'); + /** * @route GET /community/posts/:communityPostId * @desc 커뮤니티 게시글 상세 조회 * @access Private */ -module.exports = async (req, res) => { +module.exports = asyncWrapper(async (req, res) => { + const { communityPostId } = req.params; + if (!communityPostId) { + return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.BAD_REQUEST, responseMessage.NULL_VALUE)); + } + + const dbConnection = await db.connect(req); + req.dbConnection = dbConnection; + + dayjs().format(); + dayjs.extend(customParseFormat); + + const communityPost = await communityDB.getCommunityPostDetail(dbConnection, communityPostId); + if (!communityPost) { + return res.status(statusCode.NOT_FOUND).send(util.fail(statusCode.NOT_FOUND, responseMessage.NO_COMMUNITY_POST)); + } + + communityPost.createdAt = dayjs(`${communityPost.createdAt}`).format('YYYY. MM. DD'); -}; \ No newline at end of file + res.status(statusCode.OK).send( + util.success(statusCode.OK, responseMessage.READ_COMMUNITY_POST_SUCCESS, { + ...communityPost, + profileImage: dummyImages.user_profile_dummy, + }), + ); +}); diff --git a/functions/constants/dummyImages.js b/functions/constants/dummyImages.js index 77d67e6..84b78db 100644 --- a/functions/constants/dummyImages.js +++ b/functions/constants/dummyImages.js @@ -1,3 +1,4 @@ module.exports = { - content_dummy: 'https://havit-bucket.s3.ap-northeast-2.amazonaws.com/havit_content_dummy.png', -} \ No newline at end of file + content_dummy: 'https://havit-bucket.s3.ap-northeast-2.amazonaws.com/havit_content_dummy.png', + user_profile_dummy: 'https://havit-bucket.s3.ap-northeast-2.amazonaws.com/user_profile_dummy.png', +}; diff --git a/functions/constants/responseMessage.js b/functions/constants/responseMessage.js index 3eeec68..c1318fa 100644 --- a/functions/constants/responseMessage.js +++ b/functions/constants/responseMessage.js @@ -1,67 +1,71 @@ module.exports = { - NULL_VALUE: '필요한 값이 없습니다', - OUT_OF_VALUE: '파라미터 값이 잘못되었습니다', - FORBIDDEN: '허용되지 않은 접근입니다.', - - // 인증 - SIGNIN_SUCCESS: '소셜 로그인 성공', - SIGNUP_SUCCESS: '회원 가입 성공', - DELETE_USER: '회원 탈퇴 성공', - ALREADY_EMAIL: '이미 사용중인 이메일입니다.', - NO_USER: '존재하지 않는 회원입니다.', - INVALID_EMAIL: '잘못된 이메일입니다.', - TOKEN_EXPIRED: '토큰이 만료되었습니다.', - TOKEN_INVALID: '토큰이 유효하지 않습니다.', - TOKEN_EMPTY: '토큰이 없습니다.', - TOKEN_REISSUE_SUCCESS: '토큰 재발급 성공', + NULL_VALUE: '필요한 값이 없습니다', + OUT_OF_VALUE: '파라미터 값이 잘못되었습니다', + FORBIDDEN: '허용되지 않은 접근입니다.', - // 유저 - READ_ONE_USER_SUCCESS: '유저 조회 성공', - UPDATE_USER_NICKNAME_SUCCESS: '유저 수정 성공', - READ_PROFILE_SUCCESS: '프로필 조회 성공', + // 인증 + SIGNIN_SUCCESS: '소셜 로그인 성공', + SIGNUP_SUCCESS: '회원 가입 성공', + DELETE_USER: '회원 탈퇴 성공', + ALREADY_EMAIL: '이미 사용중인 이메일입니다.', + NO_USER: '존재하지 않는 회원입니다.', + INVALID_EMAIL: '잘못된 이메일입니다.', + TOKEN_EXPIRED: '토큰이 만료되었습니다.', + TOKEN_INVALID: '토큰이 유효하지 않습니다.', + TOKEN_EMPTY: '토큰이 없습니다.', + TOKEN_REISSUE_SUCCESS: '토큰 재발급 성공', - // 콘텐츠 - ADD_ONE_CONTENT_SUCCESS: '콘텐츠 생성 성공', - TOGGLE_CONTENT_SUCCESS: '콘텐츠 조회 여부 토글 성공', - READ_ALL_CONTENT_SUCCESS: '전체 콘텐츠 조회 성공', - KEYWORD_SEARCH_CONTENT_SUCCESS: '전체 콘텐츠 키워드 검색 성공', - KEYWORD_SEARCH_CATEGORY_CONTENT_SUCCESS: '카테고리 별 콘텐츠 키워드 검색 성공', - READ_RECENT_SAVED_CONTENT_SUCCESS: '최근 저장 콘텐츠 조회 성공', - READ_UNSEEN_CONTENT_SUCCESS: '봐야 하는 콘텐츠 조회 성공', - DELETE_CONTENT_SUCCESS: '콘텐츠 삭제 성공', - NO_CONTENT: '존재하지 않는 콘텐츠', - RENAME_CONTENT_SUCCESS: '콘텐츠 제목 변경 성공', - UPDATE_CONTENT_CATEGORY_SUCCESS: '콘텐츠 카테고리 변경 성공', - UPDATE_CONTENT_NOTIFICATION_SUCCESS: '콘텐츠 알림 변경 성공', - DELETE_CONTENT_NOTIFICATION_SUCCESS: '콘텐츠 알림 삭제 성공', - DUPLICATED_CONTENT: '중복된 콘텐츠', - READ_CONTENT_NOTIFICATION_SUCCESS: '콘텐츠 알림 조회 성공', + // 유저 + READ_ONE_USER_SUCCESS: '유저 조회 성공', + UPDATE_USER_NICKNAME_SUCCESS: '유저 수정 성공', + READ_PROFILE_SUCCESS: '프로필 조회 성공', - // 카테고리 - ADD_ONE_CATEGORY_SUCCESS: '카테고리 생성 완료', - READ_CATEGORY_SUCCESS: '카테고리 전체 조회 성공', - READ_CATEGORY_NAME_SUCCESS: '카테고리 이름 조회 성공', - READ_CATEGORY_CONTENT_SUCCESS: '카테고리 별 콘텐츠 조회 성공', - UPDATE_ONE_CATEGORY_SUCCESS: '카테고리 수정 성공', - DELETE_ONE_CATEGORY_SUCCESS: '카테고리 삭제 성공', - NO_CATEGORY: '존재하지 않는 카테고리', - UPDATE_CATEGORY_ORDER_SUCCESS: '카테고리 순서 변경 성공', - DUPLICATED_CATEGORY: '중복된 카테고리', - READ_CONTENT_CATEGORY_SUCCESS: '콘텐츠 소속 카테고리 조회 성공', - - // 추천 사이트 - READ_ALL_RECOMMENDATION_SUCCESS: '추천 사이트 조회 성공', - - // 서버 내 오류 - INTERNAL_SERVER_ERROR: '서버 내 오류', + // 콘텐츠 + ADD_ONE_CONTENT_SUCCESS: '콘텐츠 생성 성공', + TOGGLE_CONTENT_SUCCESS: '콘텐츠 조회 여부 토글 성공', + READ_ALL_CONTENT_SUCCESS: '전체 콘텐츠 조회 성공', + KEYWORD_SEARCH_CONTENT_SUCCESS: '전체 콘텐츠 키워드 검색 성공', + KEYWORD_SEARCH_CATEGORY_CONTENT_SUCCESS: '카테고리 별 콘텐츠 키워드 검색 성공', + READ_RECENT_SAVED_CONTENT_SUCCESS: '최근 저장 콘텐츠 조회 성공', + READ_UNSEEN_CONTENT_SUCCESS: '봐야 하는 콘텐츠 조회 성공', + DELETE_CONTENT_SUCCESS: '콘텐츠 삭제 성공', + NO_CONTENT: '존재하지 않는 콘텐츠', + RENAME_CONTENT_SUCCESS: '콘텐츠 제목 변경 성공', + UPDATE_CONTENT_CATEGORY_SUCCESS: '콘텐츠 카테고리 변경 성공', + UPDATE_CONTENT_NOTIFICATION_SUCCESS: '콘텐츠 알림 변경 성공', + DELETE_CONTENT_NOTIFICATION_SUCCESS: '콘텐츠 알림 삭제 성공', + DUPLICATED_CONTENT: '중복된 콘텐츠', + READ_CONTENT_NOTIFICATION_SUCCESS: '콘텐츠 알림 조회 성공', - // 푸시 서버 - UPDATE_FCM_TOKEN_SUCCESS: 'fcm 토큰 수정 성공', - PUSH_SERVER_ERROR: '푸시 서버 내 오류', + // 카테고리 + ADD_ONE_CATEGORY_SUCCESS: '카테고리 생성 완료', + READ_CATEGORY_SUCCESS: '카테고리 전체 조회 성공', + READ_CATEGORY_NAME_SUCCESS: '카테고리 이름 조회 성공', + READ_CATEGORY_CONTENT_SUCCESS: '카테고리 별 콘텐츠 조회 성공', + UPDATE_ONE_CATEGORY_SUCCESS: '카테고리 수정 성공', + DELETE_ONE_CATEGORY_SUCCESS: '카테고리 삭제 성공', + NO_CATEGORY: '존재하지 않는 카테고리', + UPDATE_CATEGORY_ORDER_SUCCESS: '카테고리 순서 변경 성공', + DUPLICATED_CATEGORY: '중복된 카테고리', + READ_CONTENT_CATEGORY_SUCCESS: '콘텐츠 소속 카테고리 조회 성공', - // 공지사항 - READ_NOTICES_SUCCESS: '공지사항 조회 성공', + // 추천 사이트 + READ_ALL_RECOMMENDATION_SUCCESS: '추천 사이트 조회 성공', - // 서버 상태 체크 - HEALTH_CHECK_SUCCESS: '서버 상태 정상' - }; \ No newline at end of file + // 서버 내 오류 + INTERNAL_SERVER_ERROR: '서버 내 오류', + + // 푸시 서버 + UPDATE_FCM_TOKEN_SUCCESS: 'fcm 토큰 수정 성공', + PUSH_SERVER_ERROR: '푸시 서버 내 오류', + + // 공지사항 + READ_NOTICES_SUCCESS: '공지사항 조회 성공', + + // 커뮤니티 + READ_COMMUNITY_POST_SUCCESS: '커뮤니티 게시글 상세 조회 성공', + NO_COMMUNITY_POST: '존재하지 않는 커뮤니티 게시글', + + // 서버 상태 체크 + HEALTH_CHECK_SUCCESS: '서버 상태 정상', +}; diff --git a/functions/constants/swagger/schemas/communitySchema.js b/functions/constants/swagger/schemas/communitySchema.js index 6e45dff..9b47ff7 100644 --- a/functions/constants/swagger/schemas/communitySchema.js +++ b/functions/constants/swagger/schemas/communitySchema.js @@ -24,7 +24,7 @@ const responseCommunityPostsDetailSchema = { $contentTitle: '콘텐츠 링크 제목', $contentDescription: '콘텐츠 링크 설명', $thumbnailUrl: 'https://content-thumbnail-image-url', - $createdAt: '2024-02-01', + $createdAt: '2024. 02. 01', }, }; @@ -44,7 +44,7 @@ const responseCommunityPostsSchema = { $contentTitle: '콘텐츠 링크 제목1', $contentDescription: '콘텐츠 링크 설명1', $thumbnailUrl: 'https://content-thumbnail-image-url1', - $createdAt: '2024-02-01', + $createdAt: '2024. 02. 01', }, { $id: 2, @@ -56,7 +56,7 @@ const responseCommunityPostsSchema = { $contentTitle: '콘텐츠 링크 제목2', $contentDescription: '콘텐츠 링크 설명2', $thumbnailUrl: 'https://content-thumbnail-image-url2', - $createdAt: '2024-02-02', + $createdAt: '2024. 02. 02', }, { $id: 3, @@ -68,7 +68,7 @@ const responseCommunityPostsSchema = { $contentTitle: '콘텐츠 링크 제목3', $contentDescription: '콘텐츠 링크 설명3', $thumbnailUrl: 'https://content-thumbnail-image-url3', - $createdAt: '2024-02-03', + $createdAt: '2024. 02. 03', }, ], $totalItemCount: 3, diff --git a/functions/db/community.js b/functions/db/community.js new file mode 100644 index 0000000..02adaea --- /dev/null +++ b/functions/db/community.js @@ -0,0 +1,17 @@ +const convertSnakeToCamel = require('../lib/convertSnakeToCamel'); + +const getCommunityPostDetail = async (client, communityPostId) => { + const { rows } = await client.query( + ` + SELECT cp.id, u.nickname, cp.title, cp.body, cp.content_url, cp.content_title, cp.content_description, cp.thumbnail_url, cp.created_at + FROM community_post cp + JOIN "user" u on cp.user_id = u.id + WHERE cp.id = $1 AND cp.is_deleted = FALSE + `, + [communityPostId], + ); + + return convertSnakeToCamel.keysToCamel(rows[0]); +}; + +module.exports = { getCommunityPostDetail }; diff --git a/functions/db/index.js b/functions/db/index.js index 41f4fc3..085a30e 100644 --- a/functions/db/index.js +++ b/functions/db/index.js @@ -1,8 +1,9 @@ module.exports = { - categoryDB: require('./category'), - contentDB: require('./content'), - categoryContentDB: require('./categoryContent'), - recommendationDB: require('./recommendation'), - userDB: require('./user'), - noticeDB: require('./notice'), -}; \ No newline at end of file + categoryDB: require('./category'), + contentDB: require('./content'), + categoryContentDB: require('./categoryContent'), + recommendationDB: require('./recommendation'), + userDB: require('./user'), + noticeDB: require('./notice'), + communityDB: require('./community'), +}; From 5e8b9b7647cdf6115a965a7ef20c15d926c3bd30 Mon Sep 17 00:00:00 2001 From: Hyosik Philip Joo Date: Mon, 25 Mar 2024 22:42:46 +0900 Subject: [PATCH 11/22] =?UTF-8?q?[BUGFIX]=20/community=20=EC=82=B0?= =?UTF-8?q?=ED=95=98=20=EC=97=94=EB=93=9C=ED=8F=AC=EC=9D=B8=ED=8A=B8=20?= =?UTF-8?q?=EB=A6=AC=EC=86=8C=EC=8A=A4=EB=AA=85=20=EB=B3=B5=EC=88=98?= =?UTF-8?q?=ED=98=95=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD=20(#340)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [RENAME] community category 엔드포인트 복수형인 categories로 변경 * [RENAME] 커뮤니티 카테고리 전체 조회 파일 명 복수형으로 변경 * [RENAME] community report 엔드포인트 복수형인 reports로 변경 * [DOCS] fix PR 탬플릿 bugfix로 변경 --- .github/ISSUE_TEMPLATE/{-fix-.md => -bugfix-.md} | 2 +- ...{communityCategoryGET.js => communityCategoriesGET.js} | 2 +- .../api/routes/community/communityCategoryPostsGET.js | 2 +- functions/api/routes/community/communityReportPOST.js | 2 +- functions/api/routes/community/index.js | 8 ++++---- 5 files changed, 8 insertions(+), 8 deletions(-) rename .github/ISSUE_TEMPLATE/{-fix-.md => -bugfix-.md} (89%) rename functions/api/routes/community/{communityCategoryGET.js => communityCategoriesGET.js} (75%) diff --git a/.github/ISSUE_TEMPLATE/-fix-.md b/.github/ISSUE_TEMPLATE/-bugfix-.md similarity index 89% rename from .github/ISSUE_TEMPLATE/-fix-.md rename to .github/ISSUE_TEMPLATE/-bugfix-.md index 1a8f141..d39ae92 100644 --- a/.github/ISSUE_TEMPLATE/-fix-.md +++ b/.github/ISSUE_TEMPLATE/-bugfix-.md @@ -1,5 +1,5 @@ --- -name: "[FIX]" +name: "[BUGFIX]" about: Describe this issue template's purpose here. title: '' labels: '' diff --git a/functions/api/routes/community/communityCategoryGET.js b/functions/api/routes/community/communityCategoriesGET.js similarity index 75% rename from functions/api/routes/community/communityCategoryGET.js rename to functions/api/routes/community/communityCategoriesGET.js index 455b688..2873766 100644 --- a/functions/api/routes/community/communityCategoryGET.js +++ b/functions/api/routes/community/communityCategoriesGET.js @@ -1,5 +1,5 @@ /** - * @route GET /community/category + * @route GET /community/categories * @desc 커뮤니티 카테고리 조회 * @access Public */ diff --git a/functions/api/routes/community/communityCategoryPostsGET.js b/functions/api/routes/community/communityCategoryPostsGET.js index 0248689..a90d369 100644 --- a/functions/api/routes/community/communityCategoryPostsGET.js +++ b/functions/api/routes/community/communityCategoryPostsGET.js @@ -1,5 +1,5 @@ /** - * @route GET /community/category/:communityCategoryId + * @route GET /community/categories/:communityCategoryId * @desc 커뮤니티 카테고리별 게시글 조회 * @access Private */ diff --git a/functions/api/routes/community/communityReportPOST.js b/functions/api/routes/community/communityReportPOST.js index 6e7abf6..83bb8c2 100644 --- a/functions/api/routes/community/communityReportPOST.js +++ b/functions/api/routes/community/communityReportPOST.js @@ -1,5 +1,5 @@ /** - * @route POST /community/report + * @route POST /community/reports * @desc 커뮤니티 게시글 신고 * @access Private */ diff --git a/functions/api/routes/community/index.js b/functions/api/routes/community/index.js index 60507cc..e8a4243 100644 --- a/functions/api/routes/community/index.js +++ b/functions/api/routes/community/index.js @@ -3,8 +3,8 @@ const router = express.Router(); const { checkUser } = require('../../../middlewares/auth'); router.get( - '/category', - require('./communityCategoryGET'), + '/categories', + require('./communityCategoriesGET'), /** * #swagger.summary = "커뮤니티 카테고리 전체 조회" * #swagger.responses[200] = { @@ -21,7 +21,7 @@ router.get( ); router.get( - '/category/:communityCategoryId', + '/categories/:communityCategoryId', checkUser, require('./communityCategoryPostsGET'), /** @@ -144,7 +144,7 @@ router.post( ); router.post( - '/report', + '/reports', checkUser, require('./communityReportPOST'), /** From bcc58ed2ea15669ef6fbc4d24645a8ae138be935 Mon Sep 17 00:00:00 2001 From: Hyosik Philip Joo Date: Mon, 1 Apr 2024 18:10:34 +0900 Subject: [PATCH 12/22] =?UTF-8?q?[FEAT]=20=EC=BB=A4=EB=AE=A4=EB=8B=88?= =?UTF-8?q?=ED=8B=B0=20=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC=20=EC=A0=84?= =?UTF-8?q?=EC=B2=B4=20=EC=A1=B0=ED=9A=8C=20API=20(#346)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [FEAT] 커뮤니티 카테고리 조회 성공 response message 추가 * [FEAT] 커뮤니티 카테고리 조회 성공 쿼리 구현 * [FEAT] 커뮤니티 카테고리 조회 성공 API 구현 * [DOCS] 잘못된 토큰 재발급 엔드포인트 주석 수정 * [CHORE] 관련 response message 복수형으로 변경 --- functions/api/routes/auth/reissueTokenPOST.js | 2 +- .../routes/community/communityCategoriesGET.js | 18 ++++++++++++++++-- functions/constants/responseMessage.js | 1 + functions/db/community.js | 14 +++++++++++++- 4 files changed, 31 insertions(+), 4 deletions(-) diff --git a/functions/api/routes/auth/reissueTokenPOST.js b/functions/api/routes/auth/reissueTokenPOST.js index b58506b..300f40f 100644 --- a/functions/api/routes/auth/reissueTokenPOST.js +++ b/functions/api/routes/auth/reissueTokenPOST.js @@ -8,7 +8,7 @@ const { TOKEN_INVALID, TOKEN_EXPIRED } = require('../../../constants/jwt'); const asyncWrapper = require('../../../lib/asyncWrapper'); /** - * @route POST /auth/reissue + * @route POST /auth/token * @desc 토큰 재발급 * @access Public */ diff --git a/functions/api/routes/community/communityCategoriesGET.js b/functions/api/routes/community/communityCategoriesGET.js index 2873766..5f9b8b2 100644 --- a/functions/api/routes/community/communityCategoriesGET.js +++ b/functions/api/routes/community/communityCategoriesGET.js @@ -1,9 +1,23 @@ +const util = require('../../../lib/util'); +const statusCode = require('../../../constants/statusCode'); +const responseMessage = require('../../../constants/responseMessage'); +const db = require('../../../db/db'); +const { communityDB } = require('../../../db'); +const asyncWrapper = require('../../../lib/asyncWrapper'); + /** * @route GET /community/categories * @desc 커뮤니티 카테고리 조회 * @access Public */ -module.exports = async (req, res) => { +module.exports = asyncWrapper(async (req, res) => { + const dbConnection = await db.connect(req); + req.dbConnection = dbConnection; + + const communityCategories = await communityDB.getCommunityCategories(dbConnection); -}; \ No newline at end of file + res.status(statusCode.OK).send( + util.success(statusCode.OK, responseMessage.READ_COMMUNITY_CATEGORIES_SUCCESS, communityCategories), + ); +}); \ No newline at end of file diff --git a/functions/constants/responseMessage.js b/functions/constants/responseMessage.js index c1318fa..740b7e0 100644 --- a/functions/constants/responseMessage.js +++ b/functions/constants/responseMessage.js @@ -65,6 +65,7 @@ module.exports = { // 커뮤니티 READ_COMMUNITY_POST_SUCCESS: '커뮤니티 게시글 상세 조회 성공', NO_COMMUNITY_POST: '존재하지 않는 커뮤니티 게시글', + READ_COMMUNITY_CATEGORIES_SUCCESS: '커뮤니티 카테고리 조회 성공', // 서버 상태 체크 HEALTH_CHECK_SUCCESS: '서버 상태 정상', diff --git a/functions/db/community.js b/functions/db/community.js index 02adaea..19dedaf 100644 --- a/functions/db/community.js +++ b/functions/db/community.js @@ -14,4 +14,16 @@ const getCommunityPostDetail = async (client, communityPostId) => { return convertSnakeToCamel.keysToCamel(rows[0]); }; -module.exports = { getCommunityPostDetail }; +const getCommunityCategories = async (client) => { + const { rows } = await client.query( + ` + SELECT cc.id, cc.name + FROM community_category cc + WHERE cc.is_deleted = FALSE + `, + ); + + return convertSnakeToCamel.keysToCamel(rows); +}; + +module.exports = { getCommunityPostDetail, getCommunityCategories }; From 7d23202965526a302d91b968e9ca2f0c35621dd6 Mon Sep 17 00:00:00 2001 From: Chae Jeong Ah Date: Tue, 9 Apr 2024 01:53:17 +0900 Subject: [PATCH 13/22] =?UTF-8?q?[FEAT]=20=EC=BB=A4=EB=AE=A4=EB=8B=88?= =?UTF-8?q?=ED=8B=B0=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20=EC=9E=91=EC=84=B1=20?= =?UTF-8?q?API=20=20(#347)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [CHORE]: express-validator 추가 * [CHORE]: prettier printWidth 수정 * [FEAT] req validator 추가 * [FEAT] 커뮤니티 게시글 작성 API * [REFACTOR] 변수명 수정 * [FIX] validator 위치 middleware로 이동 * [REFACTOR] validation msg nullish operator 사용하도록 수정 * [REFACTOR] validation msg type error 방지 --- functions/.prettierrc.js | 2 +- .../api/routes/community/communityPostPOST.js | 60 ++++++++++++++++++- functions/api/routes/community/index.js | 3 + functions/constants/responseMessage.js | 2 + functions/db/community.js | 57 +++++++++++++++++- functions/middlewares/validation.js | 22 +++++++ .../validator/communityValidator.js | 16 +++++ functions/middlewares/validator/index.js | 3 + functions/package-lock.json | 34 +++++++++-- functions/package.json | 1 + 10 files changed, 189 insertions(+), 11 deletions(-) create mode 100644 functions/middlewares/validation.js create mode 100644 functions/middlewares/validator/communityValidator.js create mode 100644 functions/middlewares/validator/index.js diff --git a/functions/.prettierrc.js b/functions/.prettierrc.js index d4d99f2..7da2c09 100644 --- a/functions/.prettierrc.js +++ b/functions/.prettierrc.js @@ -4,6 +4,6 @@ module.exports = { singleQuote: true, trailingComma: 'all', arrowParens: 'always', - printWidth: 200, + printWidth: 100, tabWidth: 2, }; diff --git a/functions/api/routes/community/communityPostPOST.js b/functions/api/routes/community/communityPostPOST.js index 948699e..13edd16 100644 --- a/functions/api/routes/community/communityPostPOST.js +++ b/functions/api/routes/community/communityPostPOST.js @@ -1,9 +1,63 @@ +const util = require('../../../lib/util'); +const statusCode = require('../../../constants/statusCode'); +const responseMessage = require('../../../constants/responseMessage'); +const db = require('../../../db/db'); +const { communityDB } = require('../../../db'); +const asyncWrapper = require('../../../lib/asyncWrapper'); + /** * @route POST /community/posts * @desc 커뮤니티 게시글 작성 * @access Private */ -module.exports = async (req, res) => { - const { communityPostId } = req.body; -}; +module.exports = asyncWrapper(async (req, res) => { + const { userId } = req.user; + const { + communityCategoryIds, + title, + body, + contentUrl, + contentTitle, + contentDescription, + thumbnailUrl, + } = req.body; + + const dbConnection = await db.connect(req); + req.dbConnection = dbConnection; + + const notExistingCategoryIds = await communityDB.verifyExistCategories( + dbConnection, + communityCategoryIds, + ); + if (notExistingCategoryIds) { + return res + .status(statusCode.BAD_REQUEST) + .send(util.fail(statusCode.BAD_REQUEST, responseMessage.NO_COMMUNITY_CATEGORY)); + } + + const communityPost = await communityDB.addCommunityPost( + dbConnection, + userId, + title, + body, + contentUrl, + contentTitle, + contentDescription, + thumbnailUrl, + ); + + await Promise.all( + communityCategoryIds.map(async (communityCategoryId) => { + await communityDB.addCommunityCategoryPost( + dbConnection, + communityCategoryId, + communityPost.id, + ); + }), + ); + + res + .status(statusCode.CREATED) + .send(util.success(statusCode.CREATED, responseMessage.ADD_COMMUNITY_POST_SUCCESS)); +}); diff --git a/functions/api/routes/community/index.js b/functions/api/routes/community/index.js index e8a4243..47fc6b0 100644 --- a/functions/api/routes/community/index.js +++ b/functions/api/routes/community/index.js @@ -1,6 +1,8 @@ const express = require('express'); const router = express.Router(); const { checkUser } = require('../../../middlewares/auth'); +const { validate } = require('../../../middlewares/validation'); +const { communityValidator } = require('../../../middlewares/validator'); router.get( '/categories', @@ -116,6 +118,7 @@ router.get( router.post( '/posts', checkUser, + [...communityValidator.createCommunityPostValidator, validate], require('./communityPostPOST'), /** * #swagger.summary = "커뮤니티 글 작성" diff --git a/functions/constants/responseMessage.js b/functions/constants/responseMessage.js index 740b7e0..6299db2 100644 --- a/functions/constants/responseMessage.js +++ b/functions/constants/responseMessage.js @@ -65,6 +65,8 @@ module.exports = { // 커뮤니티 READ_COMMUNITY_POST_SUCCESS: '커뮤니티 게시글 상세 조회 성공', NO_COMMUNITY_POST: '존재하지 않는 커뮤니티 게시글', + ADD_COMMUNITY_POST_SUCCESS: '커뮤니티 게시글 작성 성공', + NO_COMMUNITY_CATEGORY: '존재하지 않는 커뮤니티 카테고리', READ_COMMUNITY_CATEGORIES_SUCCESS: '커뮤니티 카테고리 조회 성공', // 서버 상태 체크 diff --git a/functions/db/community.js b/functions/db/community.js index 19dedaf..208007c 100644 --- a/functions/db/community.js +++ b/functions/db/community.js @@ -14,6 +14,55 @@ const getCommunityPostDetail = async (client, communityPostId) => { return convertSnakeToCamel.keysToCamel(rows[0]); }; +const addCommunityPost = async ( + client, + userId, + title, + body, + contentUrl, + contentTitle, + contentDescription, + thumbnailUrl, +) => { + const { rows } = await client.query( + ` + INSERT INTO community_post + (user_id, title, body, content_url, content_title, content_description, thumbnail_url) + VALUES + ($1, $2, $3, $4, $5, $6, $7) + RETURNING * + `, + [userId, title, body, contentUrl, contentTitle, contentDescription, thumbnailUrl], + ); + return convertSnakeToCamel.keysToCamel(rows[0]); +}; + +const addCommunityCategoryPost = async (client, communityCategoryId, communityPostId) => { + const { rows } = await client.query( + ` + INSERT INTO community_category_post + (community_category_id, community_post_id) + VALUES + ($1, $2) + `, + [communityCategoryId, communityPostId], + ); +}; + +const verifyExistCategories = async (client, communityCategoryIds) => { + const { rows } = await client.query( + ` + SELECT element + FROM unnest($1::int[]) AS element + LEFT JOIN community_category ON community_category.id = element + WHERE community_category.id IS NULL; + `, + [communityCategoryIds], + ); + + return convertSnakeToCamel.keysToCamel(rows[0]); +}; + const getCommunityCategories = async (client) => { const { rows } = await client.query( ` @@ -26,4 +75,10 @@ const getCommunityCategories = async (client) => { return convertSnakeToCamel.keysToCamel(rows); }; -module.exports = { getCommunityPostDetail, getCommunityCategories }; +module.exports = { + getCommunityPostDetail, + addCommunityPost, + addCommunityCategoryPost, + verifyExistCategories, + getCommunityCategories, +}; diff --git a/functions/middlewares/validation.js b/functions/middlewares/validation.js new file mode 100644 index 0000000..d66f9f4 --- /dev/null +++ b/functions/middlewares/validation.js @@ -0,0 +1,22 @@ +const { validationResult } = require('express-validator'); +const responseMessage = require('../constants/responseMessage'); +const statusCode = require('../constants/statusCode'); + +const validate = (req, res, next) => { + const errors = validationResult(req); + if (errors.isEmpty()) { + return next(); + } + + const validatorErrorMessage = errors.array()[1]?.msg ?? errors.array()[0]?.msg; + + const detailError = { + statusCode: statusCode.BAD_REQUEST, + responseMessage: responseMessage.NULL_VALUE, + validatorErrorMessage, + }; + + return next(detailError); +}; + +module.exports = { validate }; diff --git a/functions/middlewares/validator/communityValidator.js b/functions/middlewares/validator/communityValidator.js new file mode 100644 index 0000000..f92194d --- /dev/null +++ b/functions/middlewares/validator/communityValidator.js @@ -0,0 +1,16 @@ +const { body } = require('express-validator'); + +const createCommunityPostValidator = [ + body('communityCategoryIds') + .isArray() + .notEmpty() + .withMessage('Invalid communityCategoryIds field'), + body('title').isString().notEmpty().withMessage('Invalid title field'), + body('body').isString().notEmpty().withMessage('Invalid body field'), + body('contentUrl').isString().notEmpty().withMessage('Invalid contentUrl field'), + body('contentTitle').isString().notEmpty().withMessage('Invalid contentTitle field'), +]; + +module.exports = { + createCommunityPostValidator, +}; diff --git a/functions/middlewares/validator/index.js b/functions/middlewares/validator/index.js new file mode 100644 index 0000000..5ab0db3 --- /dev/null +++ b/functions/middlewares/validator/index.js @@ -0,0 +1,3 @@ +module.exports = { + communityValidator: require('./communityValidator'), +}; diff --git a/functions/package-lock.json b/functions/package-lock.json index 966a4e9..663db64 100644 --- a/functions/package-lock.json +++ b/functions/package-lock.json @@ -15,6 +15,7 @@ "dotenv": "^10.0.0", "eslint-config-prettier": "^8.3.0", "express": "^4.17.2", + "express-validator": "^7.0.1", "firebase-admin": "^10.2.0", "firebase-functions": "^3.14.1", "helmet": "^5.0.1", @@ -2138,6 +2139,18 @@ "node": ">= 0.10.0" } }, + "node_modules/express-validator": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.0.1.tgz", + "integrity": "sha512-oB+z9QOzQIE8FnlINqyIFA8eIckahC6qc8KtqLdLJcU3/phVyuhXH3bA4qzcrhme+1RYaCSwrq+TlZ/kAKIARA==", + "dependencies": { + "lodash": "^4.17.21", + "validator": "^13.9.0" + }, + "engines": { + "node": ">= 8.0.0" + } + }, "node_modules/express/node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -5074,9 +5087,9 @@ "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==" }, "node_modules/validator": { - "version": "13.7.0", - "resolved": "https://registry.npmjs.org/validator/-/validator-13.7.0.tgz", - "integrity": "sha512-nYXQLCBkpJ8X6ltALua9dRrZDHVYxjJ1wgskNt1lH9fzGjs3tgojGSCBjmEPwkWS1y29+DrizMTW19Pr9uB2nw==", + "version": "13.11.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.11.0.tgz", + "integrity": "sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==", "engines": { "node": ">= 0.10" } @@ -6984,6 +6997,15 @@ } } }, + "express-validator": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.0.1.tgz", + "integrity": "sha512-oB+z9QOzQIE8FnlINqyIFA8eIckahC6qc8KtqLdLJcU3/phVyuhXH3bA4qzcrhme+1RYaCSwrq+TlZ/kAKIARA==", + "requires": { + "lodash": "^4.17.21", + "validator": "^13.9.0" + } + }, "extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -9181,9 +9203,9 @@ "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==" }, "validator": { - "version": "13.7.0", - "resolved": "https://registry.npmjs.org/validator/-/validator-13.7.0.tgz", - "integrity": "sha512-nYXQLCBkpJ8X6ltALua9dRrZDHVYxjJ1wgskNt1lH9fzGjs3tgojGSCBjmEPwkWS1y29+DrizMTW19Pr9uB2nw==" + "version": "13.11.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.11.0.tgz", + "integrity": "sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==" }, "vary": { "version": "1.1.2", diff --git a/functions/package.json b/functions/package.json index 4bd5dc3..9ee5986 100644 --- a/functions/package.json +++ b/functions/package.json @@ -26,6 +26,7 @@ "dotenv": "^10.0.0", "eslint-config-prettier": "^8.3.0", "express": "^4.17.2", + "express-validator": "^7.0.1", "firebase-admin": "^10.2.0", "firebase-functions": "^3.14.1", "helmet": "^5.0.1", From f2a14ebaed680c8a9827e632c848753b0d4e4741 Mon Sep 17 00:00:00 2001 From: yubinquitous <65652094+yubinquitous@users.noreply.github.com> Date: Wed, 10 Apr 2024 02:29:37 +0900 Subject: [PATCH 14/22] =?UTF-8?q?[FEAT]=20=EC=BB=A4=EB=AE=A4=EB=8B=88?= =?UTF-8?q?=ED=8B=B0=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20=EC=A0=84=EC=B2=B4?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20API=20(#345)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [FIX] 게시물 전체조회/카테고리별 조회 API response에 currentPage, totalPageCount,isLastPage 추가 * [FEAT] user profile dummy image url 추가 * [FEAT] 커뮤니티 게시글 상세조회 API * [FIX] createdAt 형식 포맷팅 * [FIX] db query 결과 반환 형식 수정 * [FIX] createdAt format 수정 * [FEAT] 커뮤니티 글 전체 조회 API * [FIX] swagger options의 url 변경 * [FIX] page > totalPageCount일 경우 NOT_FOUND 에러 반환 * [FIX] query string 예외 처리 및 주석 추가 * [FIX] isLastPage 비교 조건 수정 * [FIX] 연산자를 이용해 number로 변환 Co-authored-by: Chae Jeong Ah * [FIX] COUNT 함수 ::int 이용해서 number로 형변환 Co-authored-by: Chae Jeong Ah * [FIX] ORDER BY created_at 중복 방지를 위해 ORDER BY id 추가 Co-authored-by: Chae Jeong Ah * [FIX] COUNT(*)::int로 number 변환됨에 따라 parseInt 삭제 * [FIX] 유저가 신고한 게시글 필터링 * [FIX] 커뮤니티 게시글 카운트할 때 유저가 신고한 게시글 제외 * [FIX] middleware를 이용해 query string validate --------- Co-authored-by: jokj624 --- .../api/routes/community/communityPostsGET.js | 52 +++++++++++++++- functions/api/routes/community/index.js | 1 + functions/config/swagger.js | 60 +++++++++---------- functions/constants/responseMessage.js | 2 + .../swagger/schemas/communitySchema.js | 3 + functions/db/community.js | 32 ++++++++++ .../validator/communityValidator.js | 8 ++- 7 files changed, 125 insertions(+), 33 deletions(-) diff --git a/functions/api/routes/community/communityPostsGET.js b/functions/api/routes/community/communityPostsGET.js index e486bfe..f886aa7 100644 --- a/functions/api/routes/community/communityPostsGET.js +++ b/functions/api/routes/community/communityPostsGET.js @@ -1,9 +1,57 @@ +const dayjs = require('dayjs'); +const customParseFormat = require('dayjs/plugin/customParseFormat'); +const util = require('../../../lib/util'); +const statusCode = require('../../../constants/statusCode'); +const responseMessage = require('../../../constants/responseMessage'); +const asyncWrapper = require('../../../lib/asyncWrapper'); +const db = require('../../../db/db'); +const { communityDB } = require('../../../db'); +const dummyImages = require('../../../constants/dummyImages'); + /** * @route GET /community/posts * @desc 커뮤니티 게시글 전체 조회 * @access Private */ -module.exports = async (req, res) => { +module.exports = asyncWrapper(async (req, res) => { + const { userId } = req.user; const { page, limit } = req.query; -}; + + const dbConnection = await db.connect(req); + req.dbConnection = dbConnection; + + dayjs().format(); + dayjs.extend(customParseFormat); + + const totalItemCount = await communityDB.getCommunityPostsCount(dbConnection, userId); // 총 게시글 수 + const totalPageCount = Math.ceil(totalItemCount / limit); // 총 페이지 수 + const currentPage = +page; // 현재 페이지 + const isLastPage = totalPageCount === currentPage; // 마지막 페이지인지 여부 + // 요청한 페이지가 존재하지 않는 경우 + if (page > totalPageCount) { + return res + .status(statusCode.NOT_FOUND) + .send(util.fail(statusCode.NOT_FOUND, responseMessage.NO_PAGE)); + } + + const communityPosts = await communityDB.getCommunityPosts(dbConnection, userId, limit, page); + // 각 게시글의 createdAt 형식 변경 및 프로필 이미지 추가 + const result = await Promise.all( + communityPosts.map((communityPost) => { + communityPost.createdAt = dayjs(`${communityPost.createdAt}`).format('YYYY. MM. DD'); + communityPost.profileImage = dummyImages.user_profile_dummy; + return communityPost; + }), + ); + + res.status(statusCode.OK).send( + util.success(statusCode.OK, responseMessage.READ_COMMUNITY_POSTS_SUCCESS, { + posts: result, + currentPage, + totalPageCount, + totalItemCount, + isLastPage, + }), + ); +}); diff --git a/functions/api/routes/community/index.js b/functions/api/routes/community/index.js index 47fc6b0..52f4702 100644 --- a/functions/api/routes/community/index.js +++ b/functions/api/routes/community/index.js @@ -85,6 +85,7 @@ router.get( router.get( '/posts', checkUser, + [...communityValidator.getCommunityPostsValidator, validate], require('./communityPostsGET'), /** * #swagger.summary = "커뮤니티 게시글 전체 조회" diff --git a/functions/config/swagger.js b/functions/config/swagger.js index 6e070e6..29e3e8a 100644 --- a/functions/config/swagger.js +++ b/functions/config/swagger.js @@ -4,40 +4,40 @@ const dotenv = require('dotenv'); dotenv.config({ path: '.env.dev' }); const options = { - info: { - title: 'HAVIT API Docs', - description: 'HAVIT APP server API 문서입니다', + info: { + title: 'HAVIT API Docs', + description: 'HAVIT APP server API 문서입니다', + }, + host: 'http://localhost:5001', + servers: [ + { + url: 'http://localhost:5001/havit-wesopt29/asia-northeast3/api', + description: '로컬 개발환경 host', }, - host: "http://localhost:5001", - servers: [ - { - url: 'http://localhost:5001/havit-production/asia-northeast3/api', - description: '로컬 개발환경 host', - }, - { - url: process.env.DEV_HOST, - description: '개발환경 host', - }, - ], - schemes: ['http'], - securityDefinitions: { - bearerAuth: { - type: 'http', - name: 'x-auth-token', - in: 'header', - bearerFormat: 'JWT', - }, + { + url: process.env.DEV_HOST, + description: '개발환경 host', }, - components: { - schemas: { - ...commonErrorSchema, - ...noticeSchema, - ...communitySchema, - } - } + ], + schemes: ['http'], + securityDefinitions: { + bearerAuth: { + type: 'http', + name: 'x-auth-token', + in: 'header', + bearerFormat: 'JWT', + }, + }, + components: { + schemas: { + ...commonErrorSchema, + ...noticeSchema, + ...communitySchema, + }, + }, }; const outputFile = '../constants/swagger/swagger-output.json'; const endpointsFiles = ['../api/index.js']; -swaggerAutogen(outputFile, endpointsFiles, options); \ No newline at end of file +swaggerAutogen(outputFile, endpointsFiles, options); diff --git a/functions/constants/responseMessage.js b/functions/constants/responseMessage.js index 6299db2..22f7aa8 100644 --- a/functions/constants/responseMessage.js +++ b/functions/constants/responseMessage.js @@ -64,9 +64,11 @@ module.exports = { // 커뮤니티 READ_COMMUNITY_POST_SUCCESS: '커뮤니티 게시글 상세 조회 성공', + READ_COMMUNITY_POSTS_SUCCESS: '커뮤니티 게시글 전체 조회 성공', NO_COMMUNITY_POST: '존재하지 않는 커뮤니티 게시글', ADD_COMMUNITY_POST_SUCCESS: '커뮤니티 게시글 작성 성공', NO_COMMUNITY_CATEGORY: '존재하지 않는 커뮤니티 카테고리', + NO_PAGE: '존재하지 않는 페이지', READ_COMMUNITY_CATEGORIES_SUCCESS: '커뮤니티 카테고리 조회 성공', // 서버 상태 체크 diff --git a/functions/constants/swagger/schemas/communitySchema.js b/functions/constants/swagger/schemas/communitySchema.js index 9b47ff7..e4e7784 100644 --- a/functions/constants/swagger/schemas/communitySchema.js +++ b/functions/constants/swagger/schemas/communitySchema.js @@ -71,7 +71,10 @@ const responseCommunityPostsSchema = { $createdAt: '2024. 02. 03', }, ], + $currentPage: 1, + $totalPageCount: 1, $totalItemCount: 3, + $isLastPage: true, }, }; diff --git a/functions/db/community.js b/functions/db/community.js index 208007c..dbe5253 100644 --- a/functions/db/community.js +++ b/functions/db/community.js @@ -14,6 +14,22 @@ const getCommunityPostDetail = async (client, communityPostId) => { return convertSnakeToCamel.keysToCamel(rows[0]); }; +const getCommunityPosts = async (client, userId, limit, page) => { + const { rows } = await client.query( + ` + SELECT cp.id, u.nickname, cp.title, cp.body, cp.content_url, cp.content_title, cp.content_description, cp.thumbnail_url, cp.created_at + FROM community_post cp + JOIN "user" u ON cp.user_id = u.id + LEFT JOIN community_post_report_user cpru ON cp.id = cpru.community_post_id AND cpru.report_user_id = $1 + WHERE cp.is_deleted = FALSE AND cpru.id IS NULL + ORDER BY cp.created_at DESC, cp.id DESC + LIMIT $2 OFFSET $3 + `, + [userId, limit, (page - 1) * limit], + ); + return convertSnakeToCamel.keysToCamel(rows); +}; + const addCommunityPost = async ( client, userId, @@ -75,10 +91,26 @@ const getCommunityCategories = async (client) => { return convertSnakeToCamel.keysToCamel(rows); }; +const getCommunityPostsCount = async (client, userId) => { + const { rows } = await client.query( + ` + SELECT COUNT(*)::int + FROM community_post cp + LEFT JOIN community_post_report_user cpru ON cp.id = cpru.community_post_id AND cpru.report_user_id = $1 + WHERE cp.is_deleted = FALSE AND cpru.id IS NULL + `, + [userId], + ); + + return rows[0].count; +}; + module.exports = { getCommunityPostDetail, + getCommunityPosts, addCommunityPost, addCommunityCategoryPost, verifyExistCategories, getCommunityCategories, + getCommunityPostsCount, }; diff --git a/functions/middlewares/validator/communityValidator.js b/functions/middlewares/validator/communityValidator.js index f92194d..94d00d8 100644 --- a/functions/middlewares/validator/communityValidator.js +++ b/functions/middlewares/validator/communityValidator.js @@ -1,4 +1,4 @@ -const { body } = require('express-validator'); +const { body, query } = require('express-validator'); const createCommunityPostValidator = [ body('communityCategoryIds') @@ -11,6 +11,12 @@ const createCommunityPostValidator = [ body('contentTitle').isString().notEmpty().withMessage('Invalid contentTitle field'), ]; +const getCommunityPostsValidator = [ + query('page').notEmpty().isInt({ min: 1 }).withMessage('Invalid page field'), + query('limit').notEmpty().isInt({ min: 1 }).withMessage('Invalid limit field'), +]; + module.exports = { createCommunityPostValidator, + getCommunityPostsValidator, }; From fefb4f350aebd4b5da41430a6b769b83763fd329 Mon Sep 17 00:00:00 2001 From: yubinquitous <65652094+yubinquitous@users.noreply.github.com> Date: Tue, 23 Apr 2024 04:14:46 +0900 Subject: [PATCH 15/22] =?UTF-8?q?[FEAT]=20=EC=B9=B4=ED=85=8C=EA=B3=A0?= =?UTF-8?q?=EB=A6=AC=EB=B3=84=20=EC=BB=A4=EB=AE=A4=EB=8B=88=ED=8B=B0=20?= =?UTF-8?q?=EA=B2=8C=EC=8B=9C=EA=B8=80=20=EC=A1=B0=ED=9A=8C=20API=20(#348)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [FEAT] user profile dummy image url 추가 * [FEAT] 커뮤니티 게시글 상세조회 API * [FIX] createdAt 형식 포맷팅 * [FIX] db query 결과 반환 형식 수정 * [FIX] createdAt format 수정 * [WIP] 커뮤니티 카테고리별 게시글 조회 구현 중 * [FEAT] 커뮤니티 카테고리별 게시글 조회 API 구현 * [FIX] communityCategoryId 검증 * [FIX] 불필요한 주석과 isLastPage 변수 삭제 * [FIX] getComunityCategoryPostsValidator, getCommunityCategoryPostsById로 변경 --------- Co-authored-by: jokj624 --- .../community/communityCategoryPostsGET.js | 74 ++++++++++++++++++- .../api/routes/community/communityPostsGET.js | 11 +-- functions/api/routes/community/index.js | 1 + functions/constants/responseMessage.js | 1 + functions/db/community.js | 59 ++++++++++++++- .../validator/communityValidator.js | 12 ++- 6 files changed, 148 insertions(+), 10 deletions(-) diff --git a/functions/api/routes/community/communityCategoryPostsGET.js b/functions/api/routes/community/communityCategoryPostsGET.js index a90d369..8c3b74b 100644 --- a/functions/api/routes/community/communityCategoryPostsGET.js +++ b/functions/api/routes/community/communityCategoryPostsGET.js @@ -1,9 +1,79 @@ +const dayjs = require('dayjs'); +const customParseFormat = require('dayjs/plugin/customParseFormat'); +const util = require('../../../lib/util'); +const statusCode = require('../../../constants/statusCode'); +const responseMessage = require('../../../constants/responseMessage'); +const asyncWrapper = require('../../../lib/asyncWrapper'); +const db = require('../../../db/db'); +const { communityDB } = require('../../../db'); +const dummyImages = require('../../../constants/dummyImages'); /** * @route GET /community/categories/:communityCategoryId * @desc 커뮤니티 카테고리별 게시글 조회 * @access Private */ -module.exports = async (req, res) => { +module.exports = asyncWrapper(async (req, res) => { + const { userId } = req.user; const { page, limit } = req.query; -}; + const { communityCategoryId } = req.params; + + const dbConnection = await db.connect(req); + req.dbConnection = dbConnection; + + dayjs().format(); + dayjs.extend(customParseFormat); + + // category id가 존재하지 않는 경우 + const isExistingCategory = await communityDB.isExistingCategory( + dbConnection, + communityCategoryId, + ); + if (!isExistingCategory) { + return res + .status(statusCode.NOT_FOUND) + .send(util.fail(statusCode.NOT_FOUND, responseMessage.NO_CATEGORY)); + } + + const totalItemCount = await communityDB.getCommunityCategoryPostsCount( + dbConnection, + userId, + communityCategoryId, + ); + const totalPageCount = Math.ceil(totalItemCount / limit); + const currentPage = +page; + + // 요청한 페이지가 존재하지 않는 경우 + if (page > totalPageCount) { + return res + .status(statusCode.NOT_FOUND) + .send(util.fail(statusCode.NOT_FOUND, responseMessage.NO_PAGE)); + } + + const offset = (page - 1) * limit; + const communityCategoryPosts = await communityDB.getCommunityCategoryPostsById( + dbConnection, + userId, + communityCategoryId, + limit, + offset, + ); + // 각 게시글의 createdAt 형식 변경 및 프로필 이미지 추가 + const result = await Promise.all( + communityCategoryPosts.map((communityPost) => { + communityPost.createdAt = dayjs(`${communityPost.createdAt}`).format('YYYY. MM. DD'); + communityPost.profileImage = dummyImages.user_profile_dummy; + return communityPost; + }), + ); + + res.status(statusCode.OK).send( + util.success(statusCode.OK, responseMessage.READ_COMMUNITY_CATEGORY_POSTS_SUCCESS, { + posts: result, + currentPage, + totalPageCount, + totalItemCount, + isLastPage: currentPage === totalPageCount, + }), + ); +}); diff --git a/functions/api/routes/community/communityPostsGET.js b/functions/api/routes/community/communityPostsGET.js index f886aa7..e1d423d 100644 --- a/functions/api/routes/community/communityPostsGET.js +++ b/functions/api/routes/community/communityPostsGET.js @@ -25,9 +25,9 @@ module.exports = asyncWrapper(async (req, res) => { dayjs.extend(customParseFormat); const totalItemCount = await communityDB.getCommunityPostsCount(dbConnection, userId); // 총 게시글 수 - const totalPageCount = Math.ceil(totalItemCount / limit); // 총 페이지 수 - const currentPage = +page; // 현재 페이지 - const isLastPage = totalPageCount === currentPage; // 마지막 페이지인지 여부 + const totalPageCount = Math.ceil(totalItemCount / limit); + const currentPage = +page; + // 요청한 페이지가 존재하지 않는 경우 if (page > totalPageCount) { return res @@ -35,7 +35,8 @@ module.exports = asyncWrapper(async (req, res) => { .send(util.fail(statusCode.NOT_FOUND, responseMessage.NO_PAGE)); } - const communityPosts = await communityDB.getCommunityPosts(dbConnection, userId, limit, page); + const offset = (page - 1) * limit; + const communityPosts = await communityDB.getCommunityPosts(dbConnection, userId, limit, offset); // 각 게시글의 createdAt 형식 변경 및 프로필 이미지 추가 const result = await Promise.all( communityPosts.map((communityPost) => { @@ -51,7 +52,7 @@ module.exports = asyncWrapper(async (req, res) => { currentPage, totalPageCount, totalItemCount, - isLastPage, + isLastPage: currentPage === totalPageCount, }), ); }); diff --git a/functions/api/routes/community/index.js b/functions/api/routes/community/index.js index 52f4702..db48158 100644 --- a/functions/api/routes/community/index.js +++ b/functions/api/routes/community/index.js @@ -25,6 +25,7 @@ router.get( router.get( '/categories/:communityCategoryId', checkUser, + [...communityValidator.getCommunityCategoryPostsValidator, validate], require('./communityCategoryPostsGET'), /** * #swagger.summary = "커뮤니티 카테고리별 게시글 조회" diff --git a/functions/constants/responseMessage.js b/functions/constants/responseMessage.js index 22f7aa8..46aa338 100644 --- a/functions/constants/responseMessage.js +++ b/functions/constants/responseMessage.js @@ -70,6 +70,7 @@ module.exports = { NO_COMMUNITY_CATEGORY: '존재하지 않는 커뮤니티 카테고리', NO_PAGE: '존재하지 않는 페이지', READ_COMMUNITY_CATEGORIES_SUCCESS: '커뮤니티 카테고리 조회 성공', + READ_COMMUNITY_CATEGORY_POSTS_SUCCESS: '커뮤니티 카테고리별 게시글 조회 성공', // 서버 상태 체크 HEALTH_CHECK_SUCCESS: '서버 상태 정상', diff --git a/functions/db/community.js b/functions/db/community.js index dbe5253..cf49b1e 100644 --- a/functions/db/community.js +++ b/functions/db/community.js @@ -14,7 +14,7 @@ const getCommunityPostDetail = async (client, communityPostId) => { return convertSnakeToCamel.keysToCamel(rows[0]); }; -const getCommunityPosts = async (client, userId, limit, page) => { +const getCommunityPosts = async (client, userId, limit, offset) => { const { rows } = await client.query( ` SELECT cp.id, u.nickname, cp.title, cp.body, cp.content_url, cp.content_title, cp.content_description, cp.thumbnail_url, cp.created_at @@ -25,7 +25,7 @@ const getCommunityPosts = async (client, userId, limit, page) => { ORDER BY cp.created_at DESC, cp.id DESC LIMIT $2 OFFSET $3 `, - [userId, limit, (page - 1) * limit], + [userId, limit, offset], ); return convertSnakeToCamel.keysToCamel(rows); }; @@ -79,6 +79,19 @@ const verifyExistCategories = async (client, communityCategoryIds) => { return convertSnakeToCamel.keysToCamel(rows[0]); }; +const isExistingCategory = async (client, communityCategoryId) => { + const { rows } = await client.query( + ` + SELECT 1 + FROM community_category + WHERE id = $1 AND is_deleted = FALSE + `, + [communityCategoryId], + ); + + return convertSnakeToCamel.keysToCamel(rows[0]); +}; + const getCommunityCategories = async (client) => { const { rows } = await client.query( ` @@ -105,12 +118,54 @@ const getCommunityPostsCount = async (client, userId) => { return rows[0].count; }; +const getCommunityCategoryPostsCount = async (client, userId, communityCategoryId) => { + const { rows } = await client.query( + ` + SELECT COUNT(*)::int + FROM community_post cp + JOIN community_category_post ccp ON cp.id = ccp.community_post_id + LEFT JOIN community_post_report_user cpru ON cp.id = cpru.community_post_id AND cpru.report_user_id = $1 + WHERE cp.is_deleted = FALSE AND ccp.community_category_id = $2 AND cpru.id IS NULL + `, + [userId, communityCategoryId], + ); + + return rows[0].count; +}; + +const getCommunityCategoryPostsById = async ( + client, + userId, + communityCategoryId, + limit, + offset, +) => { + const { rows } = await client.query( + ` + SELECT cp.id, u.nickname, cp.title, cp.body, cp.content_url, cp.content_title, cp.content_description, cp.thumbnail_url, cp.created_at + FROM community_post cp + JOIN "user" u ON cp.user_id = u.id + JOIN community_category_post ccp ON cp.id = ccp.community_post_id + LEFT JOIN community_post_report_user cpru ON cp.id = cpru.community_post_id AND cpru.report_user_id = $1 + WHERE cp.is_deleted = FALSE AND ccp.community_category_id = $2 AND cpru.id IS NULL + ORDER BY cp.created_at DESC, cp.id DESC + LIMIT $3 OFFSET $4 + `, + [userId, communityCategoryId, limit, offset], + ); + + return convertSnakeToCamel.keysToCamel(rows); +}; + module.exports = { getCommunityPostDetail, getCommunityPosts, addCommunityPost, addCommunityCategoryPost, verifyExistCategories, + isExistingCategory, getCommunityCategories, getCommunityPostsCount, + getCommunityCategoryPostsCount, + getCommunityCategoryPostsById, }; diff --git a/functions/middlewares/validator/communityValidator.js b/functions/middlewares/validator/communityValidator.js index 94d00d8..48a9a70 100644 --- a/functions/middlewares/validator/communityValidator.js +++ b/functions/middlewares/validator/communityValidator.js @@ -1,4 +1,4 @@ -const { body, query } = require('express-validator'); +const { body, query, param } = require('express-validator'); const createCommunityPostValidator = [ body('communityCategoryIds') @@ -16,7 +16,17 @@ const getCommunityPostsValidator = [ query('limit').notEmpty().isInt({ min: 1 }).withMessage('Invalid limit field'), ]; +const getCommunityCategoryPostsValidator = [ + query('page').notEmpty().isInt({ min: 1 }).withMessage('Invalid page field'), + query('limit').notEmpty().isInt({ min: 1 }).withMessage('Invalid limit field'), + param('communityCategoryId') + .notEmpty() + .isInt({ min: 1 }) + .withMessage('Invalid communityCategoryId field'), +]; + module.exports = { createCommunityPostValidator, getCommunityPostsValidator, + getCommunityCategoryPostsValidator, }; From 442401dbe13ed6af9c30b435ee768941d4712244 Mon Sep 17 00:00:00 2001 From: Hyosik Philip Joo Date: Tue, 23 Apr 2024 13:10:01 +0900 Subject: [PATCH 16/22] =?UTF-8?q?[FEAT]=20=EC=BB=A4=EB=AE=A4=EB=8B=88?= =?UTF-8?q?=ED=8B=B0=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20=EC=8B=A0=EA=B3=A0?= =?UTF-8?q?=ED=95=98=EA=B8=B0=20API=20(#349)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [FEAT] 커뮤니티 게시글 신고하기 validator 구현 * [DOCS] 커뮤니티 게시글 신고하기 response message 추가 * [FEAT] 커뮤니티 게시글 신고하기 query 작성 * [FEAT] 커뮤니티 게시글 신고하기 controller 구현 * [CHORE] validator id 최솟값 1로 변경 --- .../routes/community/communityReportPOST.js | 22 ++++++++++++++-- functions/api/routes/community/index.js | 1 + functions/constants/responseMessage.js | 1 + functions/db/community.js | 25 +++++++++++++++++++ .../validator/communityValidator.js | 5 ++++ 5 files changed, 52 insertions(+), 2 deletions(-) diff --git a/functions/api/routes/community/communityReportPOST.js b/functions/api/routes/community/communityReportPOST.js index 83bb8c2..1ca7310 100644 --- a/functions/api/routes/community/communityReportPOST.js +++ b/functions/api/routes/community/communityReportPOST.js @@ -1,9 +1,27 @@ +const util = require('../../../lib/util'); +const statusCode = require('../../../constants/statusCode'); +const responseMessage = require('../../../constants/responseMessage'); +const db = require('../../../db/db'); +const { communityDB } = require('../../../db'); +const asyncWrapper = require('../../../lib/asyncWrapper'); + /** * @route POST /community/reports * @desc 커뮤니티 게시글 신고 * @access Private */ -module.exports = async (req, res) => { +module.exports = asyncWrapper(async (req, res) => { + const { userId } = req.user; const { communityPostId } = req.body; -}; + + const dbConnection = await db.connect(req); + req.dbConnection = dbConnection; + + const communityPostReport = await communityDB.reportCommunityPost(dbConnection, userId, communityPostId); + if (!communityPostReport) { + return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.BAD_REQUEST, responseMessage.NO_COMMUNITY_POST)) + } + + res.status(statusCode.CREATED).send(util.success(statusCode.CREATED, responseMessage.REPORT_COMMUNITY_POST_SUCCESS)) +}); diff --git a/functions/api/routes/community/index.js b/functions/api/routes/community/index.js index db48158..b9a146d 100644 --- a/functions/api/routes/community/index.js +++ b/functions/api/routes/community/index.js @@ -151,6 +151,7 @@ router.post( router.post( '/reports', checkUser, + [...communityValidator.reportCommunityPostValidator, validate], require('./communityReportPOST'), /** * #swagger.summary = "커뮤니티 게시글 신고" diff --git a/functions/constants/responseMessage.js b/functions/constants/responseMessage.js index 46aa338..7022a1f 100644 --- a/functions/constants/responseMessage.js +++ b/functions/constants/responseMessage.js @@ -71,6 +71,7 @@ module.exports = { NO_PAGE: '존재하지 않는 페이지', READ_COMMUNITY_CATEGORIES_SUCCESS: '커뮤니티 카테고리 조회 성공', READ_COMMUNITY_CATEGORY_POSTS_SUCCESS: '커뮤니티 카테고리별 게시글 조회 성공', + REPORT_COMMUNITY_POST_SUCCESS: '커뮤니티 게시글 신고 성공', // 서버 상태 체크 HEALTH_CHECK_SUCCESS: '서버 상태 정상', diff --git a/functions/db/community.js b/functions/db/community.js index cf49b1e..c87f204 100644 --- a/functions/db/community.js +++ b/functions/db/community.js @@ -157,6 +157,30 @@ const getCommunityCategoryPostsById = async ( return convertSnakeToCamel.keysToCamel(rows); }; +const reportCommunityPost = async (client, userId, communityPostId) => { + const { rows: existingCommunityPosts } = await client.query( + ` + UPDATE community_post + SET reported_count = reported_count + 1 + WHERE id = $1 + RETURNING * + `, + [communityPostId] + ); + if (!existingCommunityPosts[0]) return existingCommunityPosts[0]; + const { rows: communityPostReports } = await client.query( + ` + INSERT INTO community_post_report_user + (report_user_id, community_post_id) + VALUES + ($1, $2) + RETURNING * + `, + [userId, communityPostId] + ); + return convertSnakeToCamel.keysToCamel(communityPostReports[0]); +} + module.exports = { getCommunityPostDetail, getCommunityPosts, @@ -168,4 +192,5 @@ module.exports = { getCommunityPostsCount, getCommunityCategoryPostsCount, getCommunityCategoryPostsById, + reportCommunityPost }; diff --git a/functions/middlewares/validator/communityValidator.js b/functions/middlewares/validator/communityValidator.js index 48a9a70..5163692 100644 --- a/functions/middlewares/validator/communityValidator.js +++ b/functions/middlewares/validator/communityValidator.js @@ -25,8 +25,13 @@ const getCommunityCategoryPostsValidator = [ .withMessage('Invalid communityCategoryId field'), ]; +const reportCommunityPostValidator = [ + body('communityPostId').isInt({ min: 1 }).notEmpty().withMessage('Invalid communityPostId') +] + module.exports = { createCommunityPostValidator, getCommunityPostsValidator, getCommunityCategoryPostsValidator, + reportCommunityPostValidator }; From 2fd50c520afa8778867eba52f9ff3beff505d5e7 Mon Sep 17 00:00:00 2001 From: Chae Jeong Ah Date: Wed, 24 Apr 2024 00:07:50 +0900 Subject: [PATCH 17/22] =?UTF-8?q?[FIX]=20=EC=BB=A4=EB=AE=A4=EB=8B=88?= =?UTF-8?q?=ED=8B=B0=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20=EC=83=81=EC=84=B8=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EC=8B=9C=20=EC=8B=A0=EA=B3=A0=ED=95=9C=20?= =?UTF-8?q?=EA=B2=8C=EC=8B=9C=EA=B8=80=20=ED=95=84=ED=84=B0=EB=A7=81=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80=20(#351)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [FIX] 커뮤니티 게시글 상세 조회 시 신고한 게시글 필터링 * [FIX] 커뮤니티 게시글 상세 조회 시 params validator 사용하도록 수정 * [FIX] 신고한 게시글 반환 메시지 변수명 수정 --- .../api/routes/community/communityPostGET.js | 19 +++++++++++++++---- functions/api/routes/community/index.js | 1 + functions/constants/responseMessage.js | 1 + functions/db/community.js | 16 +++++++++++++++- .../validator/communityValidator.js | 8 ++++++++ 5 files changed, 40 insertions(+), 5 deletions(-) diff --git a/functions/api/routes/community/communityPostGET.js b/functions/api/routes/community/communityPostGET.js index 02ab97d..ef26b3b 100644 --- a/functions/api/routes/community/communityPostGET.js +++ b/functions/api/routes/community/communityPostGET.js @@ -15,20 +15,31 @@ const asyncWrapper = require('../../../lib/asyncWrapper'); */ module.exports = asyncWrapper(async (req, res) => { + const { userId } = req.user; const { communityPostId } = req.params; - if (!communityPostId) { - return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.BAD_REQUEST, responseMessage.NULL_VALUE)); - } const dbConnection = await db.connect(req); req.dbConnection = dbConnection; + const isReportedPost = await communityDB.getReportedPostByUser( + dbConnection, + userId, + communityPostId, + ); + if (isReportedPost) { + return res + .status(statusCode.BAD_REQUEST) + .send(util.fail(statusCode.BAD_REQUEST, responseMessage.ALREADY_REPORTED_POST)); + } + dayjs().format(); dayjs.extend(customParseFormat); const communityPost = await communityDB.getCommunityPostDetail(dbConnection, communityPostId); if (!communityPost) { - return res.status(statusCode.NOT_FOUND).send(util.fail(statusCode.NOT_FOUND, responseMessage.NO_COMMUNITY_POST)); + return res + .status(statusCode.NOT_FOUND) + .send(util.fail(statusCode.NOT_FOUND, responseMessage.NO_COMMUNITY_POST)); } communityPost.createdAt = dayjs(`${communityPost.createdAt}`).format('YYYY. MM. DD'); diff --git a/functions/api/routes/community/index.js b/functions/api/routes/community/index.js index b9a146d..114e109 100644 --- a/functions/api/routes/community/index.js +++ b/functions/api/routes/community/index.js @@ -59,6 +59,7 @@ router.get( router.get( '/posts/:communityPostId', checkUser, + [...communityValidator.getCommunityPostValidator, validate], require('./communityPostGET'), /** * #swagger.summary = "커뮤니티 게시글 상세 조회" diff --git a/functions/constants/responseMessage.js b/functions/constants/responseMessage.js index 7022a1f..4a9d0e9 100644 --- a/functions/constants/responseMessage.js +++ b/functions/constants/responseMessage.js @@ -70,6 +70,7 @@ module.exports = { NO_COMMUNITY_CATEGORY: '존재하지 않는 커뮤니티 카테고리', NO_PAGE: '존재하지 않는 페이지', READ_COMMUNITY_CATEGORIES_SUCCESS: '커뮤니티 카테고리 조회 성공', + ALREADY_REPORTED_POST: '이미 신고한 게시글', READ_COMMUNITY_CATEGORY_POSTS_SUCCESS: '커뮤니티 카테고리별 게시글 조회 성공', REPORT_COMMUNITY_POST_SUCCESS: '커뮤니티 게시글 신고 성공', diff --git a/functions/db/community.js b/functions/db/community.js index c87f204..749ad20 100644 --- a/functions/db/community.js +++ b/functions/db/community.js @@ -118,6 +118,19 @@ const getCommunityPostsCount = async (client, userId) => { return rows[0].count; }; +const getReportedPostByUser = async (client, userId, communityPostId) => { + const { rows } = await client.query( + ` + SELECT 1 + FROM community_post_report_user cpru + WHERE cpru.report_user_id = $1 AND cpru.community_post_id = $2 + `, + [userId, communityPostId], + ); + + return rows[0]; +}; + const getCommunityCategoryPostsCount = async (client, userId, communityCategoryId) => { const { rows } = await client.query( ` @@ -190,7 +203,8 @@ module.exports = { isExistingCategory, getCommunityCategories, getCommunityPostsCount, + getReportedPostByUser, getCommunityCategoryPostsCount, getCommunityCategoryPostsById, - reportCommunityPost + reportCommunityPost, }; diff --git a/functions/middlewares/validator/communityValidator.js b/functions/middlewares/validator/communityValidator.js index 5163692..6b78efe 100644 --- a/functions/middlewares/validator/communityValidator.js +++ b/functions/middlewares/validator/communityValidator.js @@ -16,6 +16,13 @@ const getCommunityPostsValidator = [ query('limit').notEmpty().isInt({ min: 1 }).withMessage('Invalid limit field'), ]; +const getCommunityPostValidator = [ + param('communityPostId') + .notEmpty() + .isInt({ min: 1 }) + .withMessage('Invalid communityPostId field'), +]; + const getCommunityCategoryPostsValidator = [ query('page').notEmpty().isInt({ min: 1 }).withMessage('Invalid page field'), query('limit').notEmpty().isInt({ min: 1 }).withMessage('Invalid limit field'), @@ -32,6 +39,7 @@ const reportCommunityPostValidator = [ module.exports = { createCommunityPostValidator, getCommunityPostsValidator, + getCommunityPostValidator, getCommunityCategoryPostsValidator, reportCommunityPostValidator }; From 3f8bf9eaa5cb19b7645746c657f65cc595ec9dfc Mon Sep 17 00:00:00 2001 From: yubinquitous <65652094+yubinquitous@users.noreply.github.com> Date: Tue, 28 May 2024 14:10:38 +0900 Subject: [PATCH 18/22] =?UTF-8?q?[FIX]=20community=20=EA=B2=8C=EC=8B=9C?= =?UTF-8?q?=EB=AC=BC=20=EC=A1=B0=ED=9A=8C=20api=EC=97=90=EC=84=9C=20image?= =?UTF-8?q?=20url=20=EC=88=98=EC=A0=95=20(#354)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [FIX] community 게시글 조회 api의 contentDescription nullable로 명세 변경 * [FIX] thumbnailUrl이 null일 경우 dummy로 대체 --- functions/api/routes/community/communityPostGET.js | 1 + functions/api/routes/community/communityPostsGET.js | 4 +++- functions/constants/swagger/schemas/communitySchema.js | 8 ++++---- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/functions/api/routes/community/communityPostGET.js b/functions/api/routes/community/communityPostGET.js index ef26b3b..f2d6e2d 100644 --- a/functions/api/routes/community/communityPostGET.js +++ b/functions/api/routes/community/communityPostGET.js @@ -43,6 +43,7 @@ module.exports = asyncWrapper(async (req, res) => { } communityPost.createdAt = dayjs(`${communityPost.createdAt}`).format('YYYY. MM. DD'); + communityPost.thumbnailUrl = communityPost.thumbnailUrl || dummyImages.content_dummy; res.status(statusCode.OK).send( util.success(statusCode.OK, responseMessage.READ_COMMUNITY_POST_SUCCESS, { diff --git a/functions/api/routes/community/communityPostsGET.js b/functions/api/routes/community/communityPostsGET.js index e1d423d..701f985 100644 --- a/functions/api/routes/community/communityPostsGET.js +++ b/functions/api/routes/community/communityPostsGET.js @@ -37,11 +37,13 @@ module.exports = asyncWrapper(async (req, res) => { const offset = (page - 1) * limit; const communityPosts = await communityDB.getCommunityPosts(dbConnection, userId, limit, offset); - // 각 게시글의 createdAt 형식 변경 및 프로필 이미지 추가 + + // 각 게시글의 createdAt 형식 변경, 프로필 이미지 추가, 썸네일 이미지 null일 경우 대체 이미지 추가 const result = await Promise.all( communityPosts.map((communityPost) => { communityPost.createdAt = dayjs(`${communityPost.createdAt}`).format('YYYY. MM. DD'); communityPost.profileImage = dummyImages.user_profile_dummy; + communityPost.thumbnailUrl = communityPost.thumbnailUrl || dummyImages.content_dummy; return communityPost; }), ); diff --git a/functions/constants/swagger/schemas/communitySchema.js b/functions/constants/swagger/schemas/communitySchema.js index e4e7784..70ba77f 100644 --- a/functions/constants/swagger/schemas/communitySchema.js +++ b/functions/constants/swagger/schemas/communitySchema.js @@ -22,7 +22,7 @@ const responseCommunityPostsDetailSchema = { $body: '본문', $contentUrl: 'https://naver.com', $contentTitle: '콘텐츠 링크 제목', - $contentDescription: '콘텐츠 링크 설명', + contentDescription: '콘텐츠 링크 설명', $thumbnailUrl: 'https://content-thumbnail-image-url', $createdAt: '2024. 02. 01', }, @@ -42,7 +42,7 @@ const responseCommunityPostsSchema = { $body: '본문1', $contentUrl: 'https://naver.com', $contentTitle: '콘텐츠 링크 제목1', - $contentDescription: '콘텐츠 링크 설명1', + contentDescription: '콘텐츠 링크 설명1', $thumbnailUrl: 'https://content-thumbnail-image-url1', $createdAt: '2024. 02. 01', }, @@ -54,7 +54,7 @@ const responseCommunityPostsSchema = { $body: '본문2', $contentUrl: 'https://example2.com', $contentTitle: '콘텐츠 링크 제목2', - $contentDescription: '콘텐츠 링크 설명2', + contentDescription: '콘텐츠 링크 설명2', $thumbnailUrl: 'https://content-thumbnail-image-url2', $createdAt: '2024. 02. 02', }, @@ -66,7 +66,7 @@ const responseCommunityPostsSchema = { $body: '본문3', $contentUrl: 'https://example3.com', $contentTitle: '콘텐츠 링크 제목3', - $contentDescription: '콘텐츠 링크 설명3', + contentDescription: '콘텐츠 링크 설명3', $thumbnailUrl: 'https://content-thumbnail-image-url3', $createdAt: '2024. 02. 03', }, From 625da1ec2e2bf6d2337d2609359eb9dd539b3da3 Mon Sep 17 00:00:00 2001 From: yubinquitous <65652094+yubinquitous@users.noreply.github.com> Date: Wed, 12 Jun 2024 23:50:43 +0900 Subject: [PATCH 19/22] =?UTF-8?q?[FEAT]=20=EC=BB=A4=EB=AE=A4=EB=8B=88?= =?UTF-8?q?=ED=8B=B0=20=EA=B2=8C=EC=8B=9C=EB=AC=BC=20=EC=82=AD=EC=A0=9C=20?= =?UTF-8?q?API=20(#356)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [FEAT] 커뮤니티 게시물 삭제 API #355 * [FIX] 커뮤니티 게시물 삭제 시 community_category_post 테이블 데이터도 삭제 #355 * [FIX] statueCode 200에서 204로 변경 --- .../routes/community/communityPostDELETE.js | 39 ++++++++++++++++ functions/api/routes/community/index.js | 29 ++++++++++++ functions/constants/responseMessage.js | 1 + .../swagger/schemas/communitySchema.js | 7 +++ functions/db/community.js | 44 +++++++++++++++++-- .../validator/communityValidator.js | 14 ++++-- 6 files changed, 128 insertions(+), 6 deletions(-) create mode 100644 functions/api/routes/community/communityPostDELETE.js diff --git a/functions/api/routes/community/communityPostDELETE.js b/functions/api/routes/community/communityPostDELETE.js new file mode 100644 index 0000000..7afc6ea --- /dev/null +++ b/functions/api/routes/community/communityPostDELETE.js @@ -0,0 +1,39 @@ +const util = require('../../../lib/util'); +const statusCode = require('../../../constants/statusCode'); +const responseMessage = require('../../../constants/responseMessage'); +const db = require('../../../db/db'); +const { communityDB } = require('../../../db'); +const asyncWrapper = require('../../../lib/asyncWrapper'); + +/** + * @route DELETE /community/:communityPostId + * @desc 커뮤니티 게시글 삭제 + * @access Private + */ + +module.exports = asyncWrapper(async (req, res) => { + const { userId } = req.user; + const { communityPostId } = req.params; + + const dbConnection = await db.connect(req); + req.dbConnection = dbConnection; + + const post = await communityDB.getCommunityPostById(dbConnection, communityPostId); + if (!post) { + return res + .status(statusCode.NOT_FOUND) + .send(util.fail(statusCode.NOT_FOUND, responseMessage.NO_COMMUNITY_POST)); + } + if (post.userId !== userId) { + return res + .status(statusCode.FORBIDDEN) + .send(util.fail(statusCode.FORBIDDEN, responseMessage.FORBIDDEN)); + } + + await communityDB.deleteCommunityPostById(dbConnection, communityPostId); + await communityDB.deleteCommunityCategoryPostByPostId(dbConnection, communityPostId); + + res + .status(statusCode.NO_CONTENT) + .send(util.success(statusCode.NO_CONTENT, responseMessage.DELETE_COMMUNITY_POST_SUCCESS)); +}); diff --git a/functions/api/routes/community/index.js b/functions/api/routes/community/index.js index 114e109..6919378 100644 --- a/functions/api/routes/community/index.js +++ b/functions/api/routes/community/index.js @@ -181,4 +181,33 @@ router.post( */ ); +router.delete( + '/:communityPostId', + checkUser, + [...communityValidator.deleteCommunityPostValidator, validate], + require('./communityPostDELETE'), + /** + * #swagger.summary = "커뮤니티 게시글 삭제" + * #swagger.parameters['communityPostId'] = { + in: 'path', + description: '커뮤니티 게시글 아이디', + type: 'number', + required: true + } + * #swagger.responses[204] = { + description: "커뮤니티 게시글 삭제 성공", + content: { + "application/json": { + schema:{ + $ref: "#/components/schemas/responseDeleteCommunityPostSchema" + } + } + } + } + * #swagger.responses[400] + * #swagger.responses[403] + * #swagger.responses[404] + */ +); + module.exports = router; diff --git a/functions/constants/responseMessage.js b/functions/constants/responseMessage.js index 4a9d0e9..99581cc 100644 --- a/functions/constants/responseMessage.js +++ b/functions/constants/responseMessage.js @@ -73,6 +73,7 @@ module.exports = { ALREADY_REPORTED_POST: '이미 신고한 게시글', READ_COMMUNITY_CATEGORY_POSTS_SUCCESS: '커뮤니티 카테고리별 게시글 조회 성공', REPORT_COMMUNITY_POST_SUCCESS: '커뮤니티 게시글 신고 성공', + DELETE_COMMUNITY_POST_SUCCESS: '커뮤니티 게시글 삭제 성공', // 서버 상태 체크 HEALTH_CHECK_SUCCESS: '서버 상태 정상', diff --git a/functions/constants/swagger/schemas/communitySchema.js b/functions/constants/swagger/schemas/communitySchema.js index 70ba77f..fae2090 100644 --- a/functions/constants/swagger/schemas/communitySchema.js +++ b/functions/constants/swagger/schemas/communitySchema.js @@ -104,6 +104,12 @@ const requestCommunityReportSchema = { $communityPostId: 1, }; +const responseDeleteCommunityPostSchema = { + $status: 204, + $success: true, + $message: '커뮤니티 게시글 삭제 성공', +}; + module.exports = { responseCommunityCategorySchema, responseCommunityPostsDetailSchema, @@ -112,4 +118,5 @@ module.exports = { requestCreateCommunityPostSchema, responseCommunityReportSchema, requestCommunityReportSchema, + responseDeleteCommunityPostSchema, }; diff --git a/functions/db/community.js b/functions/db/community.js index 749ad20..f35c88d 100644 --- a/functions/db/community.js +++ b/functions/db/community.js @@ -178,7 +178,7 @@ const reportCommunityPost = async (client, userId, communityPostId) => { WHERE id = $1 RETURNING * `, - [communityPostId] + [communityPostId], ); if (!existingCommunityPosts[0]) return existingCommunityPosts[0]; const { rows: communityPostReports } = await client.query( @@ -189,10 +189,45 @@ const reportCommunityPost = async (client, userId, communityPostId) => { ($1, $2) RETURNING * `, - [userId, communityPostId] + [userId, communityPostId], ); return convertSnakeToCamel.keysToCamel(communityPostReports[0]); -} +}; + +const getCommunityPostById = async (client, communityPostId) => { + const { rows } = await client.query( + `SELECT * + FROM community_post + WHERE id = $1 AND is_deleted = FALSE + `, + [communityPostId], + ); + return convertSnakeToCamel.keysToCamel(rows[0]); +}; + +const deleteCommunityPostById = async (client, communityPostId) => { + const { rows } = await client.query( + ` + UPDATE community_post + SET is_deleted = TRUE + WHERE id = $1 + `, + [communityPostId], + ); + return convertSnakeToCamel.keysToCamel(rows[0]); +}; + +const deleteCommunityCategoryPostByPostId = async (client, communityPostId) => { + const { rows } = await client.query( + ` + UPDATE community_category_post + SET is_deleted = TRUE + WHERE community_post_id = $1 + `, + [communityPostId], + ); + return convertSnakeToCamel.keysToCamel(rows[0]); +}; module.exports = { getCommunityPostDetail, @@ -207,4 +242,7 @@ module.exports = { getCommunityCategoryPostsCount, getCommunityCategoryPostsById, reportCommunityPost, + getCommunityPostById, + deleteCommunityPostById, + deleteCommunityCategoryPostByPostId, }; diff --git a/functions/middlewares/validator/communityValidator.js b/functions/middlewares/validator/communityValidator.js index 6b78efe..8c0e23f 100644 --- a/functions/middlewares/validator/communityValidator.js +++ b/functions/middlewares/validator/communityValidator.js @@ -33,13 +33,21 @@ const getCommunityCategoryPostsValidator = [ ]; const reportCommunityPostValidator = [ - body('communityPostId').isInt({ min: 1 }).notEmpty().withMessage('Invalid communityPostId') -] + body('communityPostId').notEmpty().isInt({ min: 1 }).withMessage('Invalid communityPostId field'), +]; + +const deleteCommunityPostValidator = [ + param('communityPostId') + .notEmpty() + .isInt({ min: 1 }) + .withMessage('Invalid communityPostId field'), +]; module.exports = { createCommunityPostValidator, getCommunityPostsValidator, getCommunityPostValidator, getCommunityCategoryPostsValidator, - reportCommunityPostValidator + reportCommunityPostValidator, + deleteCommunityPostValidator, }; From fa6e6fd7d2ac85d27605ac6d1ca0c325a0bfc214 Mon Sep 17 00:00:00 2001 From: yubinquitous <65652094+yubinquitous@users.noreply.github.com> Date: Mon, 17 Jun 2024 18:29:05 +0900 Subject: [PATCH 20/22] =?UTF-8?q?[FIX]=20=EC=BB=A4=EB=AE=A4=EB=8B=88?= =?UTF-8?q?=ED=8B=B0=20=EA=B2=8C=EC=8B=9C=EB=AC=BC=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?response=EC=97=90=20=EC=9E=91=EC=84=B1=EC=9E=90=20=EB=B3=B8?= =?UTF-8?q?=EC=9D=B8=EC=97=AC=EB=B6=80=20bool=20=ED=83=80=EC=9E=85=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20(#358)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- functions/api/routes/community/communityPostGET.js | 6 +++++- functions/constants/swagger/schemas/communitySchema.js | 4 ++++ functions/db/community.js | 10 ++++++---- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/functions/api/routes/community/communityPostGET.js b/functions/api/routes/community/communityPostGET.js index f2d6e2d..8c2d073 100644 --- a/functions/api/routes/community/communityPostGET.js +++ b/functions/api/routes/community/communityPostGET.js @@ -35,7 +35,11 @@ module.exports = asyncWrapper(async (req, res) => { dayjs().format(); dayjs.extend(customParseFormat); - const communityPost = await communityDB.getCommunityPostDetail(dbConnection, communityPostId); + const communityPost = await communityDB.getCommunityPostDetail( + dbConnection, + communityPostId, + userId, + ); if (!communityPost) { return res .status(statusCode.NOT_FOUND) diff --git a/functions/constants/swagger/schemas/communitySchema.js b/functions/constants/swagger/schemas/communitySchema.js index fae2090..b6757e7 100644 --- a/functions/constants/swagger/schemas/communitySchema.js +++ b/functions/constants/swagger/schemas/communitySchema.js @@ -25,6 +25,7 @@ const responseCommunityPostsDetailSchema = { contentDescription: '콘텐츠 링크 설명', $thumbnailUrl: 'https://content-thumbnail-image-url', $createdAt: '2024. 02. 01', + $isAuthor: true, }, }; @@ -45,6 +46,7 @@ const responseCommunityPostsSchema = { contentDescription: '콘텐츠 링크 설명1', $thumbnailUrl: 'https://content-thumbnail-image-url1', $createdAt: '2024. 02. 01', + $isAuthor: true, }, { $id: 2, @@ -57,6 +59,7 @@ const responseCommunityPostsSchema = { contentDescription: '콘텐츠 링크 설명2', $thumbnailUrl: 'https://content-thumbnail-image-url2', $createdAt: '2024. 02. 02', + $isAuthor: false, }, { $id: 3, @@ -69,6 +72,7 @@ const responseCommunityPostsSchema = { contentDescription: '콘텐츠 링크 설명3', $thumbnailUrl: 'https://content-thumbnail-image-url3', $createdAt: '2024. 02. 03', + $isAuthor: false, }, ], $currentPage: 1, diff --git a/functions/db/community.js b/functions/db/community.js index f35c88d..0829934 100644 --- a/functions/db/community.js +++ b/functions/db/community.js @@ -1,14 +1,15 @@ const convertSnakeToCamel = require('../lib/convertSnakeToCamel'); -const getCommunityPostDetail = async (client, communityPostId) => { +const getCommunityPostDetail = async (client, communityPostId, userId) => { const { rows } = await client.query( ` - SELECT cp.id, u.nickname, cp.title, cp.body, cp.content_url, cp.content_title, cp.content_description, cp.thumbnail_url, cp.created_at + SELECT cp.id, u.nickname, cp.title, cp.body, cp.content_url, cp.content_title, cp.content_description, cp.thumbnail_url, cp.created_at, + CASE WHEN cp.user_id = $2 THEN TRUE ELSE FALSE END as is_author FROM community_post cp JOIN "user" u on cp.user_id = u.id WHERE cp.id = $1 AND cp.is_deleted = FALSE `, - [communityPostId], + [communityPostId, userId], ); return convertSnakeToCamel.keysToCamel(rows[0]); @@ -17,7 +18,8 @@ const getCommunityPostDetail = async (client, communityPostId) => { const getCommunityPosts = async (client, userId, limit, offset) => { const { rows } = await client.query( ` - SELECT cp.id, u.nickname, cp.title, cp.body, cp.content_url, cp.content_title, cp.content_description, cp.thumbnail_url, cp.created_at + SELECT cp.id, u.nickname, cp.title, cp.body, cp.content_url, cp.content_title, cp.content_description, cp.thumbnail_url, cp.created_at, + CASE WHEN cp.user_id = $1 THEN TRUE ELSE FALSE END as is_author FROM community_post cp JOIN "user" u ON cp.user_id = u.id LEFT JOIN community_post_report_user cpru ON cp.id = cpru.community_post_id AND cpru.report_user_id = $1 From 62299a4fd16a3ea15b5e72ecd770b4eed83ad003 Mon Sep 17 00:00:00 2001 From: yubinquitous <65652094+yubinquitous@users.noreply.github.com> Date: Tue, 25 Jun 2024 13:10:37 +0900 Subject: [PATCH 21/22] =?UTF-8?q?[FIX]=20=EC=BB=A4=EB=AE=A4=EB=8B=88?= =?UTF-8?q?=ED=8B=B0=20=EA=B2=8C=EC=8B=9C=EB=AC=BC=20=EC=B9=B4=ED=85=8C?= =?UTF-8?q?=EA=B3=A0=EB=A6=AC=EB=B3=84=20=EC=A1=B0=ED=9A=8C=20response?= =?UTF-8?q?=EC=97=90=20=EA=B2=8C=EC=8B=9C=EB=AC=BC=20=EC=9E=91=EC=84=B1?= =?UTF-8?q?=EC=9E=90=20=EB=B3=B8=EC=9D=B8=EC=97=AC=EB=B6=80=20bool=20?= =?UTF-8?q?=ED=83=80=EC=9E=85=20=EC=B6=94=EA=B0=80=20(#360)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- functions/db/community.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/functions/db/community.js b/functions/db/community.js index 0829934..0fce81d 100644 --- a/functions/db/community.js +++ b/functions/db/community.js @@ -157,7 +157,8 @@ const getCommunityCategoryPostsById = async ( ) => { const { rows } = await client.query( ` - SELECT cp.id, u.nickname, cp.title, cp.body, cp.content_url, cp.content_title, cp.content_description, cp.thumbnail_url, cp.created_at + SELECT cp.id, u.nickname, cp.title, cp.body, cp.content_url, cp.content_title, cp.content_description, cp.thumbnail_url, cp.created_at, + CASE WHEN cp.user_id = $1 THEN TRUE ELSE FALSE END as is_author FROM community_post cp JOIN "user" u ON cp.user_id = u.id JOIN community_category_post ccp ON cp.id = ccp.community_post_id From 390a0663e47321504633c2486701791536daf323 Mon Sep 17 00:00:00 2001 From: Chae Jeong Ah Date: Tue, 25 Jun 2024 13:32:53 +0900 Subject: [PATCH 22/22] =?UTF-8?q?[FEAT]=20=EC=BB=A4=EB=AE=A4=EB=8B=88?= =?UTF-8?q?=ED=8B=B0=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20=EC=8B=A0=EA=B3=A0=20?= =?UTF-8?q?=EC=8B=9C=20=EC=8A=AC=EB=9E=99=20=EC=95=8C=EB=A6=BC=20=EB=B0=9C?= =?UTF-8?q?=EC=86=A1=20(#361)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [FIX] slack message block/text type에 따른 body 수정 * [FEAT] 커뮤니티 게시글 신고 시 slack 알림 발송 * [FIX] DB 신고 쿼리에서 게시글 정보 반환하도록 수정 --- .../routes/community/communityReportPOST.js | 27 ++++++++++-- functions/db/community.js | 9 +++- functions/lib/slackMessage.js | 28 +++++++++++++ functions/middlewares/slackAPI.js | 42 +++++++++++-------- 4 files changed, 83 insertions(+), 23 deletions(-) create mode 100644 functions/lib/slackMessage.js diff --git a/functions/api/routes/community/communityReportPOST.js b/functions/api/routes/community/communityReportPOST.js index 1ca7310..e05f8e9 100644 --- a/functions/api/routes/community/communityReportPOST.js +++ b/functions/api/routes/community/communityReportPOST.js @@ -4,6 +4,8 @@ const responseMessage = require('../../../constants/responseMessage'); const db = require('../../../db/db'); const { communityDB } = require('../../../db'); const asyncWrapper = require('../../../lib/asyncWrapper'); +const { getCommunityReportMessage } = require('../../../lib/slackMessage'); +const slackAPI = require('../../../middlewares/slackAPI'); /** * @route POST /community/reports @@ -18,10 +20,27 @@ module.exports = asyncWrapper(async (req, res) => { const dbConnection = await db.connect(req); req.dbConnection = dbConnection; - const communityPostReport = await communityDB.reportCommunityPost(dbConnection, userId, communityPostId); + const communityPostReport = await communityDB.reportCommunityPost( + dbConnection, + userId, + communityPostId, + ); if (!communityPostReport) { - return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.BAD_REQUEST, responseMessage.NO_COMMUNITY_POST)) + return res + .status(statusCode.BAD_REQUEST) + .send(util.fail(statusCode.BAD_REQUEST, responseMessage.NO_COMMUNITY_POST)); } - - res.status(statusCode.CREATED).send(util.success(statusCode.CREATED, responseMessage.REPORT_COMMUNITY_POST_SUCCESS)) + + const slackReportMessage = getCommunityReportMessage( + userId, + communityPostId, + communityPostReport.title, + communityPostReport.postUserId, + ); + + slackAPI.sendMessageToSlack(slackReportMessage, slackAPI.WEB_HOOK_ERROR_MONITORING, true); + + res + .status(statusCode.CREATED) + .send(util.success(statusCode.CREATED, responseMessage.REPORT_COMMUNITY_POST_SUCCESS)); }); diff --git a/functions/db/community.js b/functions/db/community.js index 0fce81d..3c97b22 100644 --- a/functions/db/community.js +++ b/functions/db/community.js @@ -184,6 +184,9 @@ const reportCommunityPost = async (client, userId, communityPostId) => { [communityPostId], ); if (!existingCommunityPosts[0]) return existingCommunityPosts[0]; + + const { title, user_id: postUserId } = existingCommunityPosts[0]; + const { rows: communityPostReports } = await client.query( ` INSERT INTO community_post_report_user @@ -194,7 +197,11 @@ const reportCommunityPost = async (client, userId, communityPostId) => { `, [userId, communityPostId], ); - return convertSnakeToCamel.keysToCamel(communityPostReports[0]); + return { + ...convertSnakeToCamel.keysToCamel(communityPostReports[0]), + title, + postUserId, + }; }; const getCommunityPostById = async (client, communityPostId) => { diff --git a/functions/lib/slackMessage.js b/functions/lib/slackMessage.js new file mode 100644 index 0000000..d3940b6 --- /dev/null +++ b/functions/lib/slackMessage.js @@ -0,0 +1,28 @@ +const getCommunityReportMessage = (userId, postId, title, postUserId) => ` + { + "blocks": [ + { + "type": "header", + "text": { + "type": "plain_text", + "text": "🚨 게시글 신고 알림 🚨", + "emoji": true + } + }, + { + "type": "divider" + }, + { + "type": "context", + "elements": [ + { + "type": "mrkdwn", + "text": "*신고 유저 ID*: ${userId} \n *신고 게시글 ID: ${postId} * \n *게시글 제목:* ${title} \n *게시글 작성자 ID*: ${postUserId}" + } + ] + } + ] + } +`; + +module.exports = { getCommunityReportMessage }; diff --git a/functions/middlewares/slackAPI.js b/functions/middlewares/slackAPI.js index d8762b4..5db1954 100644 --- a/functions/middlewares/slackAPI.js +++ b/functions/middlewares/slackAPI.js @@ -5,25 +5,31 @@ const axios = require('axios'); // endpoint 자체는 깃허브에 올라가면 안 되기 때문! const WEB_HOOK_ERROR_MONITORING = process.env.WEB_HOOK_ERROR_MONITORING; -const sendMessageToSlack = (message, apiEndPoint = WEB_HOOK_ERROR_MONITORING) => { - // 슬랙 Webhook을 이용해 슬랙에 메시지를 보내는 코드 - try { - axios - .post(apiEndPoint, { text: message }) - .then((response) => { }) - .catch((e) => { - throw e; - }); - } catch (e) { - console.error(e); - // 슬랙 Webhook 자체에서 에러가 났을 경우, - // Firebase 콘솔에 에러를 찍는 코드 - functions.logger.error('[slackAPI 에러]', { error: e }); - } +const sendMessageToSlack = ( + message, + apiEndPoint = WEB_HOOK_ERROR_MONITORING, + isBlockMessage = false, +) => { + // 슬랙 Webhook을 이용해 슬랙에 메시지를 보내는 코드 + const body = isBlockMessage ? message : { text: message }; + + try { + axios + .post(apiEndPoint, body) + .then((response) => {}) + .catch((e) => { + throw e; + }); + } catch (e) { + console.error(e); + // 슬랙 Webhook 자체에서 에러가 났을 경우, + // Firebase 콘솔에 에러를 찍는 코드 + functions.logger.error('[slackAPI 에러]', { error: e }); + } }; // 이 파일에서 정의한 변수 / 함수를 export 해서, 다른 곳에서 사용할 수 있게 해주는 코드 module.exports = { - sendMessageToSlack, - WEB_HOOK_ERROR_MONITORING, -}; \ No newline at end of file + sendMessageToSlack, + WEB_HOOK_ERROR_MONITORING, +};