diff --git a/package.json b/package.json index 61055cd..b18877e 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,8 @@ "express": "^4.21.1", "pg": "^8.13.1", "reflect-metadata": "^0.2.2", + "swagger-jsdoc": "^6.2.8", + "swagger-ui-express": "^5.0.1", "winston": "^3.17.0", "winston-daily-rotate-file": "^5.0.0" }, @@ -46,6 +48,8 @@ "@types/node": "^22.9.0", "@types/node-fetch": "^2.6.12", "@types/pg": "^8.11.10", + "@types/swagger-jsdoc": "^6.0.4", + "@types/swagger-ui-express": "^4.1.7", "eslint": "^9.15.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-jest": "^28.9.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f33289f..9c181ba 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,6 +35,12 @@ importers: reflect-metadata: specifier: ^0.2.2 version: 0.2.2 + swagger-jsdoc: + specifier: ^6.2.8 + version: 6.2.8(openapi-types@12.1.3) + swagger-ui-express: + specifier: ^5.0.1 + version: 5.0.1(express@4.21.1) winston: specifier: ^3.17.0 version: 3.17.0 @@ -69,6 +75,12 @@ importers: '@types/pg': specifier: ^8.11.10 version: 8.11.10 + '@types/swagger-jsdoc': + specifier: ^6.0.4 + version: 6.0.4 + '@types/swagger-ui-express': + specifier: ^4.1.7 + version: 4.1.7 eslint: specifier: ^9.15.0 version: 9.15.0 @@ -115,6 +127,21 @@ packages: resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} + '@apidevtools/json-schema-ref-parser@9.1.2': + resolution: {integrity: sha512-r1w81DpR+KyRWd3f+rk6TNqMgedmAxZP5v5KWlXQWlgMUUtyEJch0DKEci1SorPMiSeM8XPl7MZ3miJ60JIpQg==} + + '@apidevtools/openapi-schemas@2.1.0': + resolution: {integrity: sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==} + engines: {node: '>=10'} + + '@apidevtools/swagger-methods@3.0.2': + resolution: {integrity: sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==} + + '@apidevtools/swagger-parser@10.0.3': + resolution: {integrity: sha512-sNiLY51vZOmSPFZA5TF35KZ2HbgYklQnTSDnkghamzLb3EkNtcQnrBQEj5AOCxHpTtXpqMCRM1CrmV2rG6nw4g==} + peerDependencies: + openapi-types: '>=7' + '@babel/code-frame@7.26.2': resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} engines: {node: '>=6.9.0'} @@ -580,6 +607,9 @@ packages: '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + '@jsdevtools/ono@7.1.3': + resolution: {integrity: sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -596,6 +626,9 @@ packages: resolution: {integrity: sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + '@scarf/scarf@1.4.0': + resolution: {integrity: sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==} + '@sinclair/typebox@0.27.8': resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} @@ -700,6 +733,12 @@ packages: '@types/stack-utils@2.0.3': resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} + '@types/swagger-jsdoc@6.0.4': + resolution: {integrity: sha512-W+Xw5epcOZrF/AooUM/PccNMSAFOKWZA5dasNyMujTwsBkU74njSJBpvCCJhHAJ95XRMzQrrW844Btu0uoetwQ==} + + '@types/swagger-ui-express@4.1.7': + resolution: {integrity: sha512-ovLM9dNincXkzH4YwyYpll75vhzPBlWx6La89wwvYH7mHjVpf0X0K/vR/aUM7SRxmr5tt9z7E5XJcjQ46q+S3g==} + '@types/triple-beam@1.3.5': resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==} @@ -921,6 +960,9 @@ packages: resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==} engines: {node: '>= 0.4'} + call-me-maybe@1.0.2: + resolution: {integrity: sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==} + callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} @@ -1017,6 +1059,10 @@ packages: resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} engines: {node: '>=18'} + commander@6.2.0: + resolution: {integrity: sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q==} + engines: {node: '>= 6'} + commander@9.5.0: resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} engines: {node: ^12.20.0 || >=14} @@ -1130,6 +1176,10 @@ packages: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} + doctrine@3.0.0: + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} + dotenv@16.4.5: resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==} engines: {node: '>=12'} @@ -1440,6 +1490,10 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} + glob@7.1.6: + resolution: {integrity: sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==} + deprecated: Glob versions prior to v9 are no longer supported + glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} deprecated: Glob versions prior to v9 are no longer supported @@ -1820,12 +1874,21 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} + lodash.get@4.4.2: + resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==} + + lodash.isequal@4.5.0: + resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==} + lodash.memoize@4.1.2: resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + lodash.mergewith@4.6.2: + resolution: {integrity: sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==} + log-update@6.1.0: resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} engines: {node: '>=18'} @@ -1980,6 +2043,9 @@ packages: resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} engines: {node: '>=18'} + openapi-types@12.1.3: + resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==} + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -2403,6 +2469,24 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + swagger-jsdoc@6.2.8: + resolution: {integrity: sha512-VPvil1+JRpmJ55CgAtn8DIcpBs0bL5L3q5bVQvF4tAW/k/9JYSj7dCpaYCAv5rufe0vcCbBRQXGvzpkWjvLklQ==} + engines: {node: '>=12.0.0'} + hasBin: true + + swagger-parser@10.0.3: + resolution: {integrity: sha512-nF7oMeL4KypldrQhac8RyHerJeGPD1p2xDh900GPvc+Nk7nWP6jX2FcC7WmkinMoAmoO774+AFXcWsW8gMWEIg==} + engines: {node: '>=10'} + + swagger-ui-dist@5.18.2: + resolution: {integrity: sha512-J+y4mCw/zXh1FOj5wGJvnAajq6XgHOyywsa9yITmwxIlJbMqITq3gYRZHaeqLVH/eV/HOPphE6NjF+nbSNC5Zw==} + + swagger-ui-express@5.0.1: + resolution: {integrity: sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==} + engines: {node: '>= v0.10.32'} + peerDependencies: + express: '>=4.0.0 || >=5.0.0-beta' + synckit@0.9.2: resolution: {integrity: sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==} engines: {node: ^14.18.0 || >=16.0.0} @@ -2611,6 +2695,10 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yaml@2.0.0-1: + resolution: {integrity: sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ==} + engines: {node: '>= 6'} + yaml@2.5.1: resolution: {integrity: sha512-bLQOjaX/ADgQ20isPJRvF0iRUHIxVhYvr53Of7wGcWlO2jvtUlH5m87DsmulFVxRpNLOnI4tB6p/oh8D7kpn9Q==} engines: {node: '>= 14'} @@ -2632,6 +2720,11 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + z-schema@5.0.5: + resolution: {integrity: sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==} + engines: {node: '>=8.0.0'} + hasBin: true + snapshots: '@ampproject/remapping@2.3.0': @@ -2639,6 +2732,27 @@ snapshots: '@jridgewell/gen-mapping': 0.3.5 '@jridgewell/trace-mapping': 0.3.25 + '@apidevtools/json-schema-ref-parser@9.1.2': + dependencies: + '@jsdevtools/ono': 7.1.3 + '@types/json-schema': 7.0.15 + call-me-maybe: 1.0.2 + js-yaml: 4.1.0 + + '@apidevtools/openapi-schemas@2.1.0': {} + + '@apidevtools/swagger-methods@3.0.2': {} + + '@apidevtools/swagger-parser@10.0.3(openapi-types@12.1.3)': + dependencies: + '@apidevtools/json-schema-ref-parser': 9.1.2 + '@apidevtools/openapi-schemas': 2.1.0 + '@apidevtools/swagger-methods': 3.0.2 + '@jsdevtools/ono': 7.1.3 + call-me-maybe: 1.0.2 + openapi-types: 12.1.3 + z-schema: 5.0.5 + '@babel/code-frame@7.26.2': dependencies: '@babel/helper-validator-identifier': 7.25.9 @@ -3158,6 +3272,8 @@ snapshots: '@jridgewell/sourcemap-codec': 1.5.0 optional: true + '@jsdevtools/ono@7.1.3': {} + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -3172,6 +3288,8 @@ snapshots: '@pkgr/core@0.1.1': {} + '@scarf/scarf@1.4.0': {} + '@sinclair/typebox@0.27.8': {} '@sinonjs/commons@3.0.1': @@ -3305,6 +3423,13 @@ snapshots: '@types/stack-utils@2.0.3': {} + '@types/swagger-jsdoc@6.0.4': {} + + '@types/swagger-ui-express@4.1.7': + dependencies: + '@types/express': 5.0.0 + '@types/serve-static': 1.15.7 + '@types/triple-beam@1.3.5': {} '@types/validator@13.12.2': {} @@ -3586,6 +3711,8 @@ snapshots: get-intrinsic: 1.2.4 set-function-length: 1.2.2 + call-me-maybe@1.0.2: {} + callsites@3.1.0: {} camelcase@5.3.1: {} @@ -3681,6 +3808,8 @@ snapshots: commander@12.1.0: {} + commander@6.2.0: {} + commander@9.5.0: {} concat-map@0.0.1: {} @@ -3770,6 +3899,10 @@ snapshots: dependencies: path-type: 4.0.0 + doctrine@3.0.0: + dependencies: + esutils: 2.0.3 + dotenv@16.4.5: {} ee-first@1.1.1: {} @@ -4129,6 +4262,15 @@ snapshots: dependencies: is-glob: 4.0.3 + glob@7.1.6: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + glob@7.2.3: dependencies: fs.realpath: 1.0.0 @@ -4683,10 +4825,16 @@ snapshots: dependencies: p-locate: 5.0.0 + lodash.get@4.4.2: {} + + lodash.isequal@4.5.0: {} + lodash.memoize@4.1.2: {} lodash.merge@4.6.2: {} + lodash.mergewith@4.6.2: {} + log-update@6.1.0: dependencies: ansi-escapes: 7.0.0 @@ -4817,6 +4965,8 @@ snapshots: dependencies: mimic-function: 5.0.1 + openapi-types@12.1.3: {} + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -5201,6 +5351,32 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + swagger-jsdoc@6.2.8(openapi-types@12.1.3): + dependencies: + commander: 6.2.0 + doctrine: 3.0.0 + glob: 7.1.6 + lodash.mergewith: 4.6.2 + swagger-parser: 10.0.3(openapi-types@12.1.3) + yaml: 2.0.0-1 + transitivePeerDependencies: + - openapi-types + + swagger-parser@10.0.3(openapi-types@12.1.3): + dependencies: + '@apidevtools/swagger-parser': 10.0.3(openapi-types@12.1.3) + transitivePeerDependencies: + - openapi-types + + swagger-ui-dist@5.18.2: + dependencies: + '@scarf/scarf': 1.4.0 + + swagger-ui-express@5.0.1(express@4.21.1): + dependencies: + express: 4.21.1 + swagger-ui-dist: 5.18.2 + synckit@0.9.2: dependencies: '@pkgr/core': 0.1.1 @@ -5409,6 +5585,8 @@ snapshots: yallist@3.1.1: {} + yaml@2.0.0-1: {} + yaml@2.5.1: {} yargs-parser@21.1.1: {} @@ -5427,3 +5605,11 @@ snapshots: optional: true yocto-queue@0.1.0: {} + + z-schema@5.0.5: + dependencies: + lodash.get: 4.4.2 + lodash.isequal: 4.5.0 + validator: 13.12.0 + optionalDependencies: + commander: 9.5.0 diff --git a/src/app.ts b/src/app.ts index 2e21e68..327750b 100644 --- a/src/app.ts +++ b/src/app.ts @@ -4,12 +4,16 @@ import dotenv from 'dotenv'; import cors from 'cors'; import cookieParser from 'cookie-parser'; import router from './routes'; +import swaggerUi from 'swagger-ui-express'; +import swaggerJSDoc from 'swagger-jsdoc'; +import { options } from '@/configs/swagger.config'; import { errorHandlingMiddleware } from './middlewares/errorHandling.middleware'; import { NotFoundError } from './exception'; dotenv.config(); const app: Application = express(); +const swaggerSpec = swaggerJSDoc(options); app.use(cookieParser()); app.use(express.json()); @@ -23,6 +27,7 @@ app.use( }), ); +app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec)); app.use('/api', router); app.use((req) => { throw new NotFoundError(`${req.url} not found`); diff --git a/src/configs/swagger.config.ts b/src/configs/swagger.config.ts new file mode 100644 index 0000000..026c769 --- /dev/null +++ b/src/configs/swagger.config.ts @@ -0,0 +1,38 @@ +export const options = { + definition: { + openapi: '3.0.0', + info: { + title: 'V.D Project API', + version: '1.0.0', + description: '모든 API는 로그인 후 진행이 가능합니다.', + }, + servers: [ + { + url: 'http://localhost:8080/api', + }, + ], + components: { + securitySchemes: { + AccessTokenAuth: { + type: 'apiKey', + description: 'API 인증을 위한 액세스 토큰입니다. 헤더, 쿠키 또는 요청 본문을 통해 전달할 수 있습니다.', + name: 'access_token', + in: 'header', + }, + RefreshTokenAuth: { + type: 'apiKey', + description: '토큰 갱신을 위한 리프레시 토큰입니다. 헤더, 쿠키 또는 요청 본문을 통해 전달할 수 있습니다.', + name: 'refresh_token', + in: 'header', + }, + }, + }, + security: [ + { + AccessTokenAuth: [], + RefreshTokenAuth: [], + }, + ], + }, + apis: ['./src/routes/*.ts', './src/types/**/*.ts'], +}; diff --git a/src/controllers/tracking.controller.ts b/src/controllers/tracking.controller.ts index 510aea5..5a115b8 100644 --- a/src/controllers/tracking.controller.ts +++ b/src/controllers/tracking.controller.ts @@ -4,7 +4,7 @@ import { TrackingService } from '@/services/tracking.service'; import { EmptyResponseDto } from '@/types'; export class TrackingController { - constructor(private trackingService: TrackingService) { } + constructor(private trackingService: TrackingService) {} event: RequestHandler = async (req: Request, res: Response, next: NextFunction) => { try { diff --git a/src/controllers/user.controller.ts b/src/controllers/user.controller.ts index 36ae3e9..1335111 100644 --- a/src/controllers/user.controller.ts +++ b/src/controllers/user.controller.ts @@ -11,11 +11,13 @@ export class UserController { const baseOptions: CookieOptions = { httpOnly: isProd, secure: isProd, - domain: process.env.COOKIE_DOMAIN || 'localhost', }; if (isProd) { baseOptions.sameSite = 'lax'; + baseOptions.domain = process.env.ALLOWED_ORIGINS; + } else { + baseOptions.domain = 'localhost'; } return baseOptions; diff --git a/src/repositories/tracking.repository.ts b/src/repositories/tracking.repository.ts index 37d745e..2e10aab 100644 --- a/src/repositories/tracking.repository.ts +++ b/src/repositories/tracking.repository.ts @@ -4,7 +4,7 @@ import { DBError } from '@/exception'; import { EventRequestDto } from '@/types'; export class TrackingRepository { - constructor(private readonly pool: Pool) { } + constructor(private readonly pool: Pool) {} async createEvent(type: EventRequestDto, id: number, req_headers: object) { try { diff --git a/src/routes/post.router.ts b/src/routes/post.router.ts index b0b82c3..b24e273 100644 --- a/src/routes/post.router.ts +++ b/src/routes/post.router.ts @@ -15,14 +15,124 @@ const postRepository = new PostRepository(pool); const postService = new PostService(postRepository); const postController = new PostController(postService); +/** + * @swagger + * /posts: + * get: + * summary: 게시물 목록 조회 + * tags: + * - Post + * parameters: + * - in: query + * name: cursor + * schema: + * $ref: '#/components/schemas/GetAllPostsQueryDto/properties/cursor' + * - in: query + * name: sort + * schema: + * $ref: '#/components/schemas/GetAllPostsQueryDto/properties/sort' + * - in: query + * name: asc + * schema: + * $ref: '#/components/schemas/GetAllPostsQueryDto/properties/asc' + * responses: + * '200': + * description: 게시물 목록 조회 성공 + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/PostsResponseDto' + * '500': + * description: 서버 오류 / 데이터 베이스 조회 오류 + */ router.get( '/posts', authMiddleware.verify, validateRequestDto(GetAllPostsQueryDto, 'query'), postController.getAllPost, ); + +/** + * @swagger + * /posts-stats: + * get: + * summary: 게시물 전체 통계 조회 + * tags: + * - Post + * responses: + * '200': + * description: 게시물 전체 통계 조회 성공 + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/PostStatisticsResponseDto' + * '500': + * description: 서버 오류 / 데이터 베이스 조회 오류 + */ router.get('/posts-stats', authMiddleware.verify, postController.getAllPostStatistics); + +/** + * @swagger + * /post/velog/{postId}: + * get: + * summary: UUID를 통한 게시물 상세 조회 + * tags: + * - Post + * parameters: + * - in: path + * name: postId + * required: true + * schema: + * type: string + * description: 조회할 게시물 UUID + * responses: + * '200': + * description: 게시물 상세 조회 성공 + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/PostResponseDto' + * '401': + * description: Post조회 실패 / Unauthorized + * '500': + * description: 서버 오류 / 데이터 베이스 조회 오류 + */ router.get('/post/velog/:postId', authMiddleware.verify, postController.getPostByUUID); + +/** + * @swagger + * /post/{postId}: + * get: + * summary: post id를 통한 게시물 상세 조회 + * tags: + * - Post + * parameters: + * - in: path + * name: postId + * required: true + * schema: + * type: integer + * description: 조회할 게시물 ID + * - in: query + * name: start + * schema: + * $ref: '#/components/schemas/GetPostQueryDto/properties/start' + * - in: query + * name: end + * schema: + * $ref: '#/components/schemas/GetPostQueryDto/properties/end' + * responses: + * '200': + * description: 게시물 상세 조회 성공 + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/PostResponseDto' + * '401': + * description: Post조회 실패 / Unauthorized + * '500': + * description: 서버 오류 / 데이터 베이스 조회 오류 + */ router.get('/post/:postId', authMiddleware.verify, postController.getPostByPostId); export default router; diff --git a/src/routes/tracking.router.ts b/src/routes/tracking.router.ts index 2a08a97..665e4c9 100644 --- a/src/routes/tracking.router.ts +++ b/src/routes/tracking.router.ts @@ -16,7 +16,58 @@ const trackingRepository = new TrackingRepository(pool); const trackingService = new TrackingService(trackingRepository); const trackingController = new TrackingController(trackingService); +/** + * @swagger + * /event: + * post: + * tags: + * - Tracking + * summary: 사용자 이벤트 등록 + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/EventRequestDto' + * responses: + * '200': + * description: 성공 + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/EmptyResponseDto' + * '400': + * description: DTO 검증 실패 / BadRequestError + * '500': + * description: 서버 오류 / 데이터 베이스 조회 오류 + */ router.post('/event', authMiddleware.verify, validateRequestDto(EventRequestDto, 'body'), trackingController.event); + +/** + * @swagger + * /stay: + * post: + * tags: + * - Tracking + * summary: 사용자 체류 시간 등록 + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/StayTimeRequestDto' + * responses: + * '200': + * description: 성공 + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/EmptyResponseDto' + * '400': + * description: DTO 검증 실패 + * '500': + * description: 서버 오류 / 데이터 베이스 조회 오류 + */ router.post('/stay', authMiddleware.verify, validateRequestDto(StayTimeRequestDto, 'body'), trackingController.stay); export default router; diff --git a/src/routes/user.router.ts b/src/routes/user.router.ts index 4206790..9cc781c 100644 --- a/src/routes/user.router.ts +++ b/src/routes/user.router.ts @@ -1,21 +1,85 @@ import express, { Router } from 'express'; +import pool from '@/configs/db.config'; import { UserController } from '@/controllers/user.controller'; import { UserRepository } from '@/repositories/user.repository'; import { UserService } from '@/services/user.service'; -import pool from '@/configs/db.config'; import { authMiddleware } from '@/middlewares/auth.middleware'; import { validateRequestDto } from '@/middlewares/validation.middleware'; -import dotenv from 'dotenv'; import { VelogUserLoginDto } from '@/types'; const router: Router = express.Router(); -dotenv.config(); const userRepository = new UserRepository(pool); const userService = new UserService(userRepository); const userController = new UserController(userService); +/** + * @swagger + * /login: + * post: + * tags: + * - User + * summary: 사용자 로그인 + * security: [] + * requestBody: + * required: false + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/LoginRequestDto' + * responses: + * '200': + * description: 성공 + * headers: + * Set-Cookie: + * schema: + * type: string + * description: 인증 쿠키 + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/LoginResponseDto' + * '400': + * description: DTO 검증 실패 + * '401': + * description: 로그인 실패 / 그룹 id 조회 실패 / 유효하지 않은 토큰 + * '500': + * description: 서버 오류 / 데이터 베이스 조회 오류 + */ router.post('/login', authMiddleware.login, validateRequestDto(VelogUserLoginDto, 'user'), userController.login); -router.post('/logout', authMiddleware.login, userController.logout); + +/** + * @swagger + * /logout: + * post: + * tags: + * - User + * summary: 사용자 로그아웃 + * responses: + * '200': + * description: 쿠키가 삭제되며 성공적으로 로그아웃함 + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/EmptyResponseDto' + */ +router.post('/logout', authMiddleware.verify, userController.logout); + +/** + * @swagger + * /me: + * get: + * tags: + * - User + * summary: 사용자 정보 조회 + * responses: + * '200': + * description: 성공 + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/LoginResponseDto' + */ router.get('/me', authMiddleware.login, userController.fetchCurrentUser); + export default router; diff --git a/src/services/tracking.service.ts b/src/services/tracking.service.ts index 29f1c6c..86187bd 100644 --- a/src/services/tracking.service.ts +++ b/src/services/tracking.service.ts @@ -4,7 +4,7 @@ import logger from '@/configs/logger.config'; import { BadRequestError } from '@/exception'; export class TrackingService { - constructor(private trackingRepo: TrackingRepository) { } + constructor(private trackingRepo: TrackingRepository) {} async tracking(eventType: EventRequestDto, id: number, req_headers: object) { return await this.trackingRepo.createEvent(eventType, id, req_headers); diff --git a/src/types/dto/requests/eventRequest.type.ts b/src/types/dto/requests/eventRequest.type.ts index 8ef9c36..1d6642e 100644 --- a/src/types/dto/requests/eventRequest.type.ts +++ b/src/types/dto/requests/eventRequest.type.ts @@ -1,13 +1,40 @@ import { IsEnum, IsNotEmpty } from 'class-validator'; +/** + * @swagger + * components: + * schemas: + * UserEventType: + * type: string + * enum: ['11', '12', '13', '21', '22', '23', '31', '99'] + * description: | + * 사용자 이벤트 타입 코드 + * * '11' - 로그인 성공 + * * '12' - 페이지 이동 + * * '13' - 로그아웃 + * * '21' - 메인 페이지 - 통계 블록 열림/닫힘 + * * '22' - 메인 페이지 - 정렬(오름차순, 방식) 선택 + * * '23' - 메인 페이지 - 새로고침 버튼 + * * '31' - 리더보드 페이지 - 정렬 방식 선택 + * * '99' - 해당 없음 + * EventRequestDto: + * type: object + * required: + * - eventType + * properties: + * eventType: + * $ref: '#/components/schemas/UserEventType' + * example: + * eventType: '01' + */ export enum UserEventType { - LOGIN = '11', // 로그인 성공 - NAVIGATE = '12', // 페이지 이동 (헤더 클릭 등) - LOGOUT = '13', // 로그아웃 - SECTION_INTERACT_MAIN = '21', // 메인 페이지 - 통계 블록 열림/닫힘 - SORT_INTERACT_MAIN = '22', // 메인 페이지 - 정렬(오름차순, 방식) 선택 - REFRESH_INTERACT_MAIN = '23', // 메인 페이지 - 새로고침 버튼 - SORT_INTERACT_BOARD = '31', // 리더보드 페이지 - 정렬 방식 선택 + LOGIN = '11', + NAVIGATE = '12', + LOGOUT = '13', + SECTION_INTERACT_MAIN = '21', + SORT_INTERACT_MAIN = '22', + REFRESH_INTERACT_MAIN = '23', + SORT_INTERACT_BOARD = '31', NOTHING = '99', } diff --git a/src/types/dto/requests/getAllPostsQuery.type.ts b/src/types/dto/requests/getAllPostsQuery.type.ts index ee6bec4..9fefc4d 100644 --- a/src/types/dto/requests/getAllPostsQuery.type.ts +++ b/src/types/dto/requests/getAllPostsQuery.type.ts @@ -1,6 +1,20 @@ import { Transform } from 'class-transformer'; import { IsBoolean, IsOptional, IsString } from 'class-validator'; +/** + * @swagger + * components: + * schemas: + * PostSortType: + * type: string + * enum: ['', 'dailyViewCount', 'dailyLikeCount'] + * description: | + * 포스트 정렬 기준 + * * '' - 작성일 + * * 'dailyViewCount' - 조회수 + * * 'dailyLikeCount' - 좋아요수 + * default: '' + */ export type PostSortType = '' | 'dailyViewCount' | 'dailyLikeCount'; export interface GetAllPostsQuery { @@ -9,6 +23,27 @@ export interface GetAllPostsQuery { asc?: boolean; } +/** + * @swagger + * components: + * schemas: + * GetAllPostsQueryDto: + * type: object + * properties: + * cursor: + * type: string + * description: 다음 페이지 조회를 위한 커서값 + * nullable: true + * sort: + * $ref: '#/components/schemas/PostSortType' + * description: 포스트 정렬 기준 + * nullable: true + * asc: + * type: boolean + * description: 오름차순 정렬 여부 + * nullable: true + * default: false + */ export class GetAllPostsQueryDto { @IsOptional() @IsString() diff --git a/src/types/dto/requests/getPostQuery.type.ts b/src/types/dto/requests/getPostQuery.type.ts index 084e87e..778ed48 100644 --- a/src/types/dto/requests/getPostQuery.type.ts +++ b/src/types/dto/requests/getPostQuery.type.ts @@ -1,7 +1,7 @@ import { Type } from 'class-transformer'; import { IsDate, IsOptional } from 'class-validator'; -export interface PostParam { +export interface PostParam extends Record { postId: string; } @@ -10,6 +10,24 @@ export interface GetPostQuery { end?: string; } +/** + * @swagger + * components: + * schemas: + * GetPostQueryDto: + * type: object + * properties: + * start: + * type: string + * format: date + * description: 조회 시작 날짜 + * nullable: true + * end: + * type: string + * format: date + * description: 조회 종료 날짜 + * nullable: true + */ export class GetPostQueryDto { @IsOptional() @IsDate() diff --git a/src/types/dto/requests/loginRequest.type.ts b/src/types/dto/requests/loginRequest.type.ts new file mode 100644 index 0000000..faf53e4 --- /dev/null +++ b/src/types/dto/requests/loginRequest.type.ts @@ -0,0 +1,33 @@ +import { IsNotEmpty, IsString } from 'class-validator'; + +/** + * @swagger + * components: + * schemas: + * LoginRequestDto: + * type: object + * required: + * - accessToken + * - refreshToken + * properties: + * accessToken: + * type: string + * description: accessToken + * refreshToken: + * type: string + * description: refreshToken + */ +export class LoginRequestDto { + @IsString() + @IsNotEmpty() + accessToken: string; + + @IsString() + @IsNotEmpty() + refreshToken: string; + + constructor(accessToken: string, refreshToken: string) { + this.accessToken = accessToken; + this.refreshToken = refreshToken; + } +} diff --git a/src/types/dto/requests/stayTimeRequest.dto.ts b/src/types/dto/requests/stayTimeRequest.dto.ts index 7b4a145..14777de 100644 --- a/src/types/dto/requests/stayTimeRequest.dto.ts +++ b/src/types/dto/requests/stayTimeRequest.dto.ts @@ -1,5 +1,26 @@ import { IsISO8601, IsNotEmpty } from 'class-validator'; +/** + * @swagger + * components: + * schemas: + * StayTimeRequestDto: + * type: object + * required: + * - loadDate + * - unloadDate + * properties: + * loadDate: + * type: string + * format: date-time + * description: 시작 날짜 + * example: '2024-01-01' + * unloadDate: + * type: string + * format: date-time + * description: 종료 날짜 + * example: '2024-01-03' + */ export class StayTimeRequestDto { @IsISO8601() @IsNotEmpty() diff --git a/src/types/dto/responses/baseResponse.type.ts b/src/types/dto/responses/baseResponse.type.ts index 29e0a2b..bdedb96 100644 --- a/src/types/dto/responses/baseResponse.type.ts +++ b/src/types/dto/responses/baseResponse.type.ts @@ -1,10 +1,37 @@ +/** + * @swagger + * components: + * schemas: + * BaseResponseDto: + * type: object + * required: + * - success + * - message + * - data + * - error + * properties: + * success: + * type: boolean + * description: 요청 성공 여부 + * message: + * type: string + * description: 응답 메시지 + * data: + * type: object + * description: 응답 데이터 (구체적인 타입은 각 엔드포인트에서 정의) + * nullable: true + * error: + * type: string + * description: 에러 메시지 + * nullable: true + */ export class BaseResponseDto { success: boolean; message: string; - data?: T; - error?: string | null; + data: T; + error: string | null; - constructor(success: boolean, message: string, data?: T, error?: string | null) { + constructor(success: boolean, message: string, data: T, error: string | null) { this.success = success; this.message = message; this.data = data; diff --git a/src/types/dto/responses/emptyReponse.type.ts b/src/types/dto/responses/emptyReponse.type.ts index e708962..27dce3b 100644 --- a/src/types/dto/responses/emptyReponse.type.ts +++ b/src/types/dto/responses/emptyReponse.type.ts @@ -1,4 +1,17 @@ import { BaseResponseDto } from '@/types/dto/responses/baseResponse.type'; type EmptyResponseData = Record; +/** + * @swagger + * components: + * schemas: + * EmptyResponseDto: + * allOf: + * - $ref: '#/components/schemas/BaseResponseDto' + * - type: object + * properties: + * data: + * type: object + * properties: {} + */ export class EmptyResponseDto extends BaseResponseDto {} diff --git a/src/types/dto/responses/loginResponse.type.ts b/src/types/dto/responses/loginResponse.type.ts index f842020..c3c1cc0 100644 --- a/src/types/dto/responses/loginResponse.type.ts +++ b/src/types/dto/responses/loginResponse.type.ts @@ -1,13 +1,52 @@ import { BaseResponseDto } from '@/types/dto/responses/baseResponse.type'; +/** + * @swagger + * components: + * schemas: + * ProfileType: + * type: object + * properties: + * thumbnail: + * type: string + * description: 프로필 이미지 URL + */ interface ProfileType { thumbnail: string; } +/** + * @swagger + * components: + * schemas: + * LoginResponseData: + * type: object + * properties: + * id: + * type: integer + * description: 사용자 ID + * username: + * type: string + * description: 사용자 이름 + * profile: + * $ref: '#/components/schemas/ProfileType' + */ interface LoginResponseData { id: number; username: string; profile: ProfileType; } +/** + * @swagger + * components: + * schemas: + * LoginResponseDto: + * allOf: + * - $ref: '#/components/schemas/BaseResponseDto' + * - type: object + * properties: + * data: + * $ref: '#/components/schemas/LoginResponseData' + */ export class LoginResponseDto extends BaseResponseDto {} diff --git a/src/types/dto/responses/postResponse.type.ts b/src/types/dto/responses/postResponse.type.ts index c2410b3..7c1275e 100644 --- a/src/types/dto/responses/postResponse.type.ts +++ b/src/types/dto/responses/postResponse.type.ts @@ -1,6 +1,40 @@ import { BaseResponseDto } from '@/types/dto/responses/baseResponse.type'; // ------ 전체 조회 ------ +/** + * @swagger + * components: + * schemas: + * GetAllPostType: + * type: object + * properties: + * id: + * type: integer + * description: 게시물 ID + * title: + * type: string + * description: 게시물 제목` + * views: + * type: integer + * description: 총 조회수 + * likes: + * type: integer + * description: 총 좋아요수 + * yesterdayViews: + * type: integer + * description: 어제 조회수 + * yesterdayLikes: + * type: integer + * description: 어제 좋아요수 + * createdAt: + * type: string + * format: date-time + * description: 생성일시 + * releasedAt: + * type: string + * format: date-time + * description: 공개일시 + */ interface GetAllPostType { id: number; title: string; @@ -12,20 +46,81 @@ interface GetAllPostType { releasedAt: string; } +/** + * @swagger + * components: + * schemas: + * PostsResponseData: + * type: object + * properties: + * nextCursor: + * type: string + * nullable: true + * description: 다음 페이지 커서값 + * posts: + * type: array + * items: + * $ref: '#/components/schemas/GetAllPostType' + */ interface PostsResponseData { nextCursor: string | null; posts: GetAllPostType[]; } +/** + * @swagger + * components: + * schemas: + * PostsResponseDto: + * allOf: + * - $ref: '#/components/schemas/BaseResponseDto' + * - type: object + * properties: + * data: + * $ref: '#/components/schemas/PostsResponseData' + */ + export class PostsResponseDto extends BaseResponseDto {} // ------ 단건 조회 ------ +/** + * @swagger + * components: + * schemas: + * GetPostType: + * type: object + * properties: + * date: + * type: string + * format: date + * description: 통계 날짜 + * dailyViewCount: + * type: integer + * description: 일일 조회수 + * dailyLikeCount: + * type: integer + * description: 일일 좋아요수 + */ interface GetPostType { date: Date; dailyViewCount: number; dailyLikeCount: number; } +/** + * @swagger + * components: + * schemas: + * PostResponseData: + * type: object + * properties: + * post: + * type: array + * nullable: true + * description: 기간 별 조회된 포스터 통계 + * items: + * $ref: '#/components/schemas/GetPostType' + */ export interface RawPostType { date: Date; daily_view_count: number; @@ -35,9 +130,45 @@ interface PostResponseData { post: GetPostType[]; } +/** + * @swagger + * components: + * schemas: + * PostResponseDto: + * allOf: + * - $ref: '#/components/schemas/BaseResponseDto' + * - type: object + * properties: + * data: + * $ref: '#/components/schemas/PostResponseData' + */ export class PostResponseDto extends BaseResponseDto {} -// ------ 전체 통계 ------ +// ------ 전체 통계 조회 ------ +/** + * @swagger + * components: + * schemas: + * PostStatisticsType: + * type: object + * properties: + * totalViews: + * type: integer + * description: 전체 조회수 + * totalLikes: + * type: integer + * description: 전체 좋아요수 + * yesterdayViews: + * type: integer + * description: 어제 조회수 + * yesterdayLikes: + * type: integer + * description: 어제 좋아요수 + * lastUpdatedDate: + * type: string + * format: date-time + * description: 마지막 업데이트 일시 + */ interface PostStatisticsType { totalViews: number; totalLikes: number; @@ -46,9 +177,34 @@ interface PostStatisticsType { lastUpdatedDate: string; } +/** + * @swagger + * components: + * schemas: + * PostStatisticsData: + * type: object + * properties: + * totalPostCount: + * type: integer + * description: 전체 게시물 수 + * stats: + * $ref: '#/components/schemas/PostStatisticsType' + */ interface PostStatisticsData { totalPostCount: number; stats: PostStatisticsType; } +/** + * @swagger + * components: + * schemas: + * PostStatisticsResponseDto: + * allOf: + * - $ref: '#/components/schemas/BaseResponseDto' + * - type: object + * properties: + * data: + * $ref: '#/components/schemas/PostStatisticsData' + */ export class PostStatisticsResponseDto extends BaseResponseDto {}