diff --git a/package-lock.json b/package-lock.json index da3c8b5..7018ee7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,7 +21,11 @@ "multer": "^2.0.2", "passport": "^0.7.0", "passport-jwt": "^4.0.1", - "passport-kakao": "^1.0.1" + "passport-kakao": "^1.0.1", + "path": "^0.12.7", + "swagger-autogen": "^2.23.7", + "swagger-ui-express": "^5.0.1", + "yamljs": "^0.3.0" }, "devDependencies": { "@aws-sdk/client-s3": "^3.971.0", @@ -1305,6 +1309,13 @@ "react-dom": "^18.0.0 || ^19.0.0" } }, + "node_modules/@scarf/scarf": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", + "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", + "hasInstallScript": true, + "license": "Apache-2.0" + }, "node_modules/@smithy/abort-controller": { "version": "4.2.8", "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.8.tgz", @@ -2239,6 +2250,18 @@ "node": ">= 0.6" } }, + "node_modules/acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -2273,7 +2296,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, "license": "MIT" }, "node_modules/binary-extensions": { @@ -2324,7 +2346,6 @@ "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -2531,7 +2552,6 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, "license": "MIT" }, "node_modules/concat-stream": { @@ -2664,6 +2684,15 @@ } } }, + "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==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/deepmerge-ts": { "version": "7.1.5", "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", @@ -2827,6 +2856,7 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", + "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -2983,6 +3013,12 @@ "node": ">= 0.8" } }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -3079,6 +3115,27 @@ "giget": "dist/cli.mjs" } }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/glob-parent": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", @@ -3212,6 +3269,17 @@ "dev": true, "license": "ISC" }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -3321,6 +3389,18 @@ "node": ">= 20" } }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/jsonwebtoken": { "version": "9.0.3", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", @@ -3539,7 +3619,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -3868,6 +3947,25 @@ "node": ">= 0.4.0" } }, + "node_modules/path": { + "version": "0.12.7", + "resolved": "https://registry.npmjs.org/path/-/path-0.12.7.tgz", + "integrity": "sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q==", + "license": "MIT", + "dependencies": { + "process": "^0.11.1", + "util": "^0.10.3" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -3990,6 +4088,15 @@ } } }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/proper-lockfile": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", @@ -4414,6 +4521,12 @@ "node": ">=10" } }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "license": "BSD-3-Clause" + }, "node_modules/sqlstring": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", @@ -4483,6 +4596,42 @@ "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==", + "license": "MIT", + "dependencies": { + "acorn": "^7.4.1", + "deepmerge": "^4.2.2", + "glob": "^7.1.7", + "json5": "^2.2.3" + } + }, + "node_modules/swagger-ui-dist": { + "version": "5.31.0", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.31.0.tgz", + "integrity": "sha512-zSUTIck02fSga6rc0RZP3b7J7wgHXwLea8ZjgLA3Vgnb8QeOl3Wou2/j5QkzSGeoz6HusP/coYuJl33aQxQZpg==", + "license": "Apache-2.0", + "dependencies": { + "@scarf/scarf": "=1.4.0" + } + }, + "node_modules/swagger-ui-express": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz", + "integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==", + "license": "MIT", + "dependencies": { + "swagger-ui-dist": ">=5.0.0" + }, + "engines": { + "node": ">= v0.10.32" + }, + "peerDependencies": { + "express": ">=4.0.0 || >=5.0.0-beta" + } + }, "node_modules/tinyexec": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", @@ -4593,12 +4742,27 @@ "node": ">= 0.8" } }, + "node_modules/util": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", + "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==", + "license": "MIT", + "dependencies": { + "inherits": "2.0.3" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, + "node_modules/util/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "license": "ISC" + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -4663,6 +4827,29 @@ "node": ">=0.4" } }, + "node_modules/yamljs": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/yamljs/-/yamljs-0.3.0.tgz", + "integrity": "sha512-C/FsVVhht4iPQYXOInoxUM/1ELSf9EsgKH34FofQOp6hwCPrW4vG4w5++TED3xRUo8gD7l0P1J1dLlDYzODsTQ==", + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "glob": "^7.0.5" + }, + "bin": { + "json2yaml": "bin/json2yaml", + "yaml2json": "bin/yaml2json" + } + }, + "node_modules/yamljs/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, "node_modules/zeptomatch": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/zeptomatch/-/zeptomatch-2.0.2.tgz", diff --git a/package.json b/package.json index 3296b61..e850cae 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,11 @@ "multer": "^2.0.2", "passport": "^0.7.0", "passport-jwt": "^4.0.1", - "passport-kakao": "^1.0.1" + "passport-kakao": "^1.0.1", + "path": "^0.12.7", + "swagger-autogen": "^2.23.7", + "swagger-ui-express": "^5.0.1", + "yamljs": "^0.3.0" }, "devDependencies": { "@aws-sdk/client-s3": "^3.971.0", diff --git a/src/config/swagger.config.js b/src/config/swagger.config.js new file mode 100644 index 0000000..e69de29 diff --git a/src/index.js b/src/index.js index 5658360..a05299c 100644 --- a/src/index.js +++ b/src/index.js @@ -6,6 +6,10 @@ import { stateHandler } from "./middlewares/state.middleware.js"; import { corsOptions } from "./config/cors.config.js"; import apiRouter from "./routes/index.js"; import prisma from "./db.config.js"; +import { swaggerHandler } from "./middlewares/swagger.middleware.js"; +import swaggerUi from "swagger-ui-express"; +import YAML from "yamljs"; +import path from "path"; dotenv.config(); @@ -26,8 +30,16 @@ app.get("/", (req, res) => { return res.success("아싸 나이스 성공~"); }); +//swagger +const swaggerDocument = YAML.load( + path.join(process.cwd(), "src/swagger/swagger.yml") +); +app.use("/docs", swaggerUi.serve, swaggerUi.setup(swaggerDocument)); + +app.get("/openapi.json", swaggerHandler); + // API 라우터 등록 -app.use("/api/v1", apiRouter); // 모든 API는 /api prefix를 가짐 +app.use("/api/v1", apiRouter); app.use(errorHandler); diff --git a/src/middlewares/swagger.middleware.js b/src/middlewares/swagger.middleware.js new file mode 100644 index 0000000..f5e3037 --- /dev/null +++ b/src/middlewares/swagger.middleware.js @@ -0,0 +1,38 @@ +import swaggerAutogen from "swagger-autogen"; + +export const swaggerHandler = async (req, res, next) => { + // #swagger.ignore = true + const options = { + openapi: "3.0.0", + disableLogs: true, + writeOutputFile: false, + }; + const outputFile = "/dev/null"; + const routes = ["./src/index.js"]; + const protocol = req.protocol; + const host = req.get("host"); + const doc = { + info: { + title: "제목", + description: "설명", + }, + host: `${protocol}://${host}`, + components: { + securitySchemes: { + bearerAuth: { + type: "http", + scheme: "bearer", + bearerFormat: "JWT", + }, + }, + }, + security: [ + { + bearerAuth: [], + }, + ], + }; + + const result = await swaggerAutogen(options)(outputFile, routes, doc); + res.json(result ? result.data : null); +}; \ No newline at end of file diff --git a/src/routes/alarm.route.js b/src/routes/alarm.route.js index ebd0884..ce6cf45 100644 --- a/src/routes/alarm.route.js +++ b/src/routes/alarm.route.js @@ -14,11 +14,14 @@ import authenticate from "../middlewares/authenticate.middleware.js"; const router = express.Router(); // GET /v1/api/alarm - 알람 목록 조회 + router.get("/", authenticate, handleAlarmList); // DELETE /v1/api/alarm/:alarmId - 개별 알림 삭제 + router.delete("/:alarmId", authenticate, handleAlarmDelete); // DELETE /v1/api/alarm - 전체 알림 삭제 (모든 알림) + router.delete("/", authenticate, handleAlarmDeleteAll); // PATCH /v1/api/alarm/settings/deadline - 최종 마감 알림 수정 diff --git a/src/swagger/swagger.yml b/src/swagger/swagger.yml new file mode 100644 index 0000000..68edd36 --- /dev/null +++ b/src/swagger/swagger.yml @@ -0,0 +1,130 @@ +openapi: 3.0.0 +info: + title: UMC 9th + description: UMC 9th Node.js 테스트 프로젝트입니다. + version: 1.0.0 + +servers: + - url: http://localhost:3000 + description: Local Development Server + +components: + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + +tags: + - name: Task + description: 과제 관련 API + - name: Alarm + description: 알림 관련 API + - name: User + description: 사용자 관련 API + - name: Folder + description: 폴더 관련 API + - name: Comment + description: 댓글 관련 API + - name: Modal + description: 모달/자료 관련 API + - name: Kakao Auth + description: 카카오 인증 관련 API + +paths: + api/v1/alarm: + get: + tags: + - Alarm + summary: 알림 목록 조회 + description: 사용자의 알림 목록을 조회합니다. + security: + - bearerAuth: [] + parameters: + - in: query + name: cursor + schema: + type: integer + required: false + description: 페이징 커서 + - in: query + name: limit + schema: + type: integer + required: false + description: 조회 개수 + default: 10 + responses: + '200': + description: 성공 + content: + application/json: + schema: + type: object + properties: + resultType: + type: string + example: SUCCESS + data: + type: array + items: + type: object + + delete: + tags: + - Alarm + summary: 전체 알림 삭제 + description: 사용자의 모든 알림을 삭제합니다. + security: + - bearerAuth: [] + responses: + '200': + description: 성공 + + api/v1/alarm/{alarmId}: + delete: + tags: + - Alarm + summary: 개별 알림 삭제 + description: 특정 알림을 삭제합니다. + security: + - bearerAuth: [] + parameters: + - in: path + name: alarmId + required: true + schema: + type: integer + description: 알림 ID + responses: + '200': + description: 성공 + + patch: + tags: + - Alarm + summary: 알림 읽음 처리 + description: 알림을 읽음으로 표시합니다. + security: + - bearerAuth: [] + parameters: + - in: path + name: alarmId + required: true + schema: + type: integer + description: 알림 ID + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + isRead: + type: boolean + example: true + responses: + '200': + description: 성공 +