diff --git a/api.hyungi.net/config/swagger.js b/api.hyungi.net/config/swagger.js new file mode 100644 index 0000000..ce2621a --- /dev/null +++ b/api.hyungi.net/config/swagger.js @@ -0,0 +1,497 @@ +// config/swagger.js - Swagger/OpenAPI 설정 + +const swaggerJSDoc = require('swagger-jsdoc'); + +const swaggerDefinition = { + openapi: '3.0.0', + info: { + title: 'Technical Korea Work Management API', + version: '2.1.0', + description: '보안이 강화된 생산관리 시스템 API - 작업자, 프로젝트, 일일 작업 보고서 관리', + contact: { + name: 'Technical Korea', + email: 'admin@technicalkorea.com' + }, + license: { + name: 'MIT', + url: 'https://opensource.org/licenses/MIT' + } + }, + servers: [ + { + url: 'http://localhost:20005', + description: '개발 서버 (Docker)' + }, + { + url: 'http://localhost:3005', + description: '로컬 개발 서버' + } + ], + components: { + securitySchemes: { + bearerAuth: { + type: 'http', + scheme: 'bearer', + bearerFormat: 'JWT', + description: 'JWT 토큰을 사용한 인증. 로그인 후 받은 토큰을 "Bearer {token}" 형식으로 입력하세요.' + } + }, + schemas: { + // 공통 응답 스키마 + SuccessResponse: { + type: 'object', + properties: { + success: { + type: 'boolean', + example: true + }, + message: { + type: 'string', + example: '요청이 성공적으로 처리되었습니다.' + }, + data: { + type: 'object', + description: '응답 데이터' + }, + timestamp: { + type: 'string', + format: 'date-time', + example: '2024-01-01T00:00:00.000Z' + } + } + }, + ErrorResponse: { + type: 'object', + properties: { + success: { + type: 'boolean', + example: false + }, + error: { + type: 'string', + example: '오류 메시지' + }, + timestamp: { + type: 'string', + format: 'date-time', + example: '2024-01-01T00:00:00.000Z' + } + } + }, + PaginatedResponse: { + type: 'object', + properties: { + success: { + type: 'boolean', + example: true + }, + message: { + type: 'string', + example: '데이터 조회 성공' + }, + data: { + type: 'array', + items: { + type: 'object' + } + }, + meta: { + type: 'object', + properties: { + pagination: { + type: 'object', + properties: { + currentPage: { type: 'integer', example: 1 }, + totalPages: { type: 'integer', example: 10 }, + totalCount: { type: 'integer', example: 100 }, + limit: { type: 'integer', example: 10 }, + hasNextPage: { type: 'boolean', example: true }, + hasPrevPage: { type: 'boolean', example: false } + } + } + } + }, + timestamp: { + type: 'string', + format: 'date-time' + } + } + }, + + // 사용자 관련 스키마 + User: { + type: 'object', + properties: { + user_id: { + type: 'integer', + example: 1, + description: '사용자 ID' + }, + username: { + type: 'string', + example: 'admin', + description: '사용자명' + }, + name: { + type: 'string', + example: '관리자', + description: '실명' + }, + email: { + type: 'string', + format: 'email', + example: 'admin@technicalkorea.com', + description: '이메일 주소' + }, + role: { + type: 'string', + example: 'admin', + description: '역할' + }, + access_level: { + type: 'string', + enum: ['user', 'admin', 'system'], + example: 'admin', + description: '접근 권한 레벨' + }, + worker_id: { + type: 'integer', + example: 1, + description: '연결된 작업자 ID' + }, + is_active: { + type: 'boolean', + example: true, + description: '활성 상태' + }, + last_login_at: { + type: 'string', + format: 'date-time', + description: '마지막 로그인 시간' + }, + created_at: { + type: 'string', + format: 'date-time', + description: '생성 시간' + }, + updated_at: { + type: 'string', + format: 'date-time', + description: '수정 시간' + } + } + }, + + // 작업자 관련 스키마 + Worker: { + type: 'object', + properties: { + worker_id: { + type: 'integer', + example: 1, + description: '작업자 ID' + }, + worker_name: { + type: 'string', + example: '김철수', + description: '작업자 이름' + }, + position: { + type: 'string', + example: '용접공', + description: '직책' + }, + department: { + type: 'string', + example: '생산부', + description: '부서' + }, + phone: { + type: 'string', + example: '010-1234-5678', + description: '전화번호' + }, + email: { + type: 'string', + format: 'email', + example: 'worker@technicalkorea.com', + description: '이메일' + }, + hire_date: { + type: 'string', + format: 'date', + example: '2024-01-01', + description: '입사일' + }, + is_active: { + type: 'boolean', + example: true, + description: '활성 상태' + }, + created_at: { + type: 'string', + format: 'date-time', + description: '생성 시간' + }, + updated_at: { + type: 'string', + format: 'date-time', + description: '수정 시간' + } + } + }, + + // 프로젝트 관련 스키마 + Project: { + type: 'object', + properties: { + project_id: { + type: 'integer', + example: 1, + description: '프로젝트 ID' + }, + project_name: { + type: 'string', + example: '신규 플랜트 건설', + description: '프로젝트 이름' + }, + description: { + type: 'string', + example: '대형 화학 플랜트 건설 프로젝트', + description: '프로젝트 설명' + }, + start_date: { + type: 'string', + format: 'date', + example: '2024-01-01', + description: '시작일' + }, + end_date: { + type: 'string', + format: 'date', + example: '2024-12-31', + description: '종료일' + }, + status: { + type: 'string', + example: 'active', + description: '프로젝트 상태' + }, + created_at: { + type: 'string', + format: 'date-time', + description: '생성 시간' + }, + updated_at: { + type: 'string', + format: 'date-time', + description: '수정 시간' + } + } + }, + + // 작업 관련 스키마 + Task: { + type: 'object', + properties: { + task_id: { + type: 'integer', + example: 1, + description: '작업 ID' + }, + task_name: { + type: 'string', + example: '용접 작업', + description: '작업 이름' + }, + description: { + type: 'string', + example: '파이프 용접 작업', + description: '작업 설명' + }, + category: { + type: 'string', + example: '용접', + description: '작업 카테고리' + }, + is_active: { + type: 'boolean', + example: true, + description: '활성 상태' + }, + created_at: { + type: 'string', + format: 'date-time', + description: '생성 시간' + }, + updated_at: { + type: 'string', + format: 'date-time', + description: '수정 시간' + } + } + }, + + // 일일 작업 보고서 관련 스키마 + DailyWorkReport: { + type: 'object', + properties: { + id: { + type: 'integer', + example: 1, + description: '보고서 ID' + }, + report_date: { + type: 'string', + format: 'date', + example: '2024-01-01', + description: '작업 날짜' + }, + worker_id: { + type: 'integer', + example: 1, + description: '작업자 ID' + }, + project_id: { + type: 'integer', + example: 1, + description: '프로젝트 ID' + }, + work_type_id: { + type: 'integer', + example: 1, + description: '작업 유형 ID' + }, + work_status_id: { + type: 'integer', + example: 1, + description: '작업 상태 ID (1:정규, 2:에러)' + }, + error_type_id: { + type: 'integer', + example: null, + description: '에러 유형 ID (에러일 때만)' + }, + work_hours: { + type: 'number', + format: 'decimal', + example: 8.5, + description: '작업 시간' + }, + created_by: { + type: 'integer', + example: 1, + description: '작성자 user_id' + }, + created_at: { + type: 'string', + format: 'date-time', + description: '생성 시간' + }, + updated_at: { + type: 'string', + format: 'date-time', + description: '수정 시간' + }, + // 조인된 데이터 + worker_name: { + type: 'string', + example: '김철수', + description: '작업자 이름' + }, + project_name: { + type: 'string', + example: '신규 플랜트 건설', + description: '프로젝트 이름' + }, + work_type_name: { + type: 'string', + example: '용접', + description: '작업 유형 이름' + }, + work_status_name: { + type: 'string', + example: '정규', + description: '작업 상태 이름' + }, + error_type_name: { + type: 'string', + example: null, + description: '에러 유형 이름' + } + } + }, + + // 로그인 관련 스키마 + LoginRequest: { + type: 'object', + required: ['username', 'password'], + properties: { + username: { + type: 'string', + example: 'admin', + description: '사용자명' + }, + password: { + type: 'string', + example: 'password123', + description: '비밀번호' + } + } + }, + LoginResponse: { + type: 'object', + properties: { + success: { + type: 'boolean', + example: true + }, + message: { + type: 'string', + example: '로그인 성공' + }, + data: { + type: 'object', + properties: { + user: { + $ref: '#/components/schemas/User' + }, + token: { + type: 'string', + example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...', + description: 'JWT 토큰' + }, + redirectUrl: { + type: 'string', + example: '/pages/dashboard/group-leader.html', + description: '리다이렉트 URL' + } + } + }, + timestamp: { + type: 'string', + format: 'date-time' + } + } + } + } + }, + security: [ + { + bearerAuth: [] + } + ] +}; + +const options = { + definition: swaggerDefinition, + apis: [ + './routes/*.js', + './controllers/*.js', + './index.js' + ] +}; + +const swaggerSpec = swaggerJSDoc(options); + +module.exports = swaggerSpec; diff --git a/api.hyungi.net/index.js b/api.hyungi.net/index.js index 6fa9e7b..818c6bf 100644 --- a/api.hyungi.net/index.js +++ b/api.hyungi.net/index.js @@ -9,6 +9,10 @@ const rateLimit = require('express-rate-limit'); const { errorMiddleware } = require('./utils/errorHandler'); const { responseMiddleware } = require('./utils/responseFormatter'); +// Swagger 설정 +const swaggerUi = require('swagger-ui-express'); +const swaggerSpec = require('./config/swagger'); + const app = express(); // 헬스체크와 개발용 엔드포인트는 CORS 이후에 등록 @@ -322,6 +326,27 @@ app.use('/api/tools', toolsRoute); // 📤 파일 업로드 app.use('/api', uploadBgRoutes); +// ===== 📚 Swagger API 문서 ===== +app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec, { + explorer: true, + customCss: '.swagger-ui .topbar { display: none }', + customSiteTitle: 'TK Work Management API', + swaggerOptions: { + persistAuthorization: true, + displayRequestDuration: true, + docExpansion: 'none', + filter: true, + showExtensions: true, + showCommonExtensions: true + } +})); + +// Swagger JSON 스펙 제공 +app.get('/api-docs.json', (req, res) => { + res.setHeader('Content-Type', 'application/json'); + res.send(swaggerSpec); +}); + // ===== 🚨 에러 핸들러 (모든 라우트 뒤에 위치) ===== app.use(errorMiddleware); diff --git a/api.hyungi.net/package-lock.json b/api.hyungi.net/package-lock.json index a328e0a..7fbde7e 100644 --- a/api.hyungi.net/package-lock.json +++ b/api.hyungi.net/package-lock.json @@ -22,7 +22,53 @@ "mysql2": "^3.14.1", "pm2": "^5.3.0", "qrcode": "^1.5.4", - "sqlite3": "^5.1.6" + "sqlite3": "^5.1.6", + "swagger-jsdoc": "^6.2.8", + "swagger-ui-express": "^5.0.1" + } + }, + "node_modules/@apidevtools/json-schema-ref-parser": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.1.2.tgz", + "integrity": "sha512-r1w81DpR+KyRWd3f+rk6TNqMgedmAxZP5v5KWlXQWlgMUUtyEJch0DKEci1SorPMiSeM8XPl7MZ3miJ60JIpQg==", + "license": "MIT", + "dependencies": { + "@jsdevtools/ono": "^7.1.3", + "@types/json-schema": "^7.0.6", + "call-me-maybe": "^1.0.1", + "js-yaml": "^4.1.0" + } + }, + "node_modules/@apidevtools/openapi-schemas": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz", + "integrity": "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/@apidevtools/swagger-methods": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz", + "integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==", + "license": "MIT" + }, + "node_modules/@apidevtools/swagger-parser": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.0.3.tgz", + "integrity": "sha512-sNiLY51vZOmSPFZA5TF35KZ2HbgYklQnTSDnkghamzLb3EkNtcQnrBQEj5AOCxHpTtXpqMCRM1CrmV2rG6nw4g==", + "license": "MIT", + "dependencies": { + "@apidevtools/json-schema-ref-parser": "^9.0.6", + "@apidevtools/openapi-schemas": "^2.0.4", + "@apidevtools/swagger-methods": "^3.0.2", + "@jsdevtools/ono": "^7.1.3", + "call-me-maybe": "^1.0.1", + "z-schema": "^5.0.1" + }, + "peerDependencies": { + "openapi-types": ">=7" } }, "node_modules/@gar/promisify": { @@ -37,6 +83,12 @@ "resolved": "https://registry.npmjs.org/@hexagon/base64/-/base64-1.1.28.tgz", "integrity": "sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw==" }, + "node_modules/@jsdevtools/ono": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", + "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", + "license": "MIT" + }, "node_modules/@levischuck/tiny-cbor": { "version": "0.2.11", "resolved": "https://registry.npmjs.org/@levischuck/tiny-cbor/-/tiny-cbor-0.2.11.tgz", @@ -342,6 +394,13 @@ "debug": "^4.3.1" } }, + "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/@simplewebauthn/server": { "version": "13.1.1", "resolved": "https://registry.npmjs.org/@simplewebauthn/server/-/server-13.1.1.tgz", @@ -374,6 +433,12 @@ "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==" }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "license": "MIT" + }, "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -612,8 +677,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "license": "MIT", - "optional": true + "license": "MIT" }, "node_modules/base64-js": { "version": "1.5.1", @@ -748,7 +812,6 @@ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "license": "MIT", - "optional": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -892,6 +955,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/call-me-maybe": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz", + "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==", + "license": "MIT" + }, "node_modules/camelcase": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", @@ -1015,8 +1084,7 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "license": "MIT", - "optional": true + "license": "MIT" }, "node_modules/concat-stream": { "version": "1.6.2", @@ -1235,6 +1303,18 @@ "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==" }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/dotenv": { "version": "16.5.0", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", @@ -1682,8 +1762,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "license": "ISC", - "optional": true + "license": "ISC" }, "node_modules/fsevents": { "version": "2.3.3", @@ -2037,7 +2116,6 @@ "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", - "optional": true, "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -2267,6 +2345,13 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead.", + "license": "MIT" + }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", @@ -2277,6 +2362,13 @@ "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", + "license": "MIT" + }, "node_modules/lodash.isinteger": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", @@ -2297,6 +2389,12 @@ "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" }, + "node_modules/lodash.mergewith": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", + "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==", + "license": "MIT" + }, "node_modules/lodash.once": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", @@ -2449,7 +2547,6 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "license": "ISC", - "optional": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -2861,6 +2958,13 @@ "wrappy": "1" } }, + "node_modules/openapi-types": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", + "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", + "license": "MIT", + "peer": true + }, "node_modules/p-limit": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", @@ -3012,7 +3116,6 @@ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", "license": "MIT", - "optional": true, "engines": { "node": ">=0.10.0" } @@ -4031,6 +4134,92 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/swagger-jsdoc": { + "version": "6.2.8", + "resolved": "https://registry.npmjs.org/swagger-jsdoc/-/swagger-jsdoc-6.2.8.tgz", + "integrity": "sha512-VPvil1+JRpmJ55CgAtn8DIcpBs0bL5L3q5bVQvF4tAW/k/9JYSj7dCpaYCAv5rufe0vcCbBRQXGvzpkWjvLklQ==", + "license": "MIT", + "dependencies": { + "commander": "6.2.0", + "doctrine": "3.0.0", + "glob": "7.1.6", + "lodash.mergewith": "^4.6.2", + "swagger-parser": "^10.0.3", + "yaml": "2.0.0-1" + }, + "bin": { + "swagger-jsdoc": "bin/swagger-jsdoc.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/swagger-jsdoc/node_modules/commander": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz", + "integrity": "sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/swagger-jsdoc/node_modules/glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "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.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/swagger-parser": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/swagger-parser/-/swagger-parser-10.0.3.tgz", + "integrity": "sha512-nF7oMeL4KypldrQhac8RyHerJeGPD1p2xDh900GPvc+Nk7nWP6jX2FcC7WmkinMoAmoO774+AFXcWsW8gMWEIg==", + "license": "MIT", + "dependencies": { + "@apidevtools/swagger-parser": "10.0.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/swagger-ui-dist": { + "version": "5.30.1", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.30.1.tgz", + "integrity": "sha512-4mNAUM31sr52K3JcK9qiGbfsFKNh/dm3PkEe+F9FAM31YY/NoRYUgsR/L6d7LLFn6PgZXtBG2ygp8+7UnpUIPg==", + "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/systeminformation": { "version": "5.27.1", "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.27.1.tgz", @@ -4389,6 +4578,15 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "license": "ISC" }, + "node_modules/yaml": { + "version": "2.0.0-1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.0.0-1.tgz", + "integrity": "sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, "node_modules/yargs": { "version": "15.4.1", "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", @@ -4421,6 +4619,36 @@ "engines": { "node": ">=6" } + }, + "node_modules/z-schema": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz", + "integrity": "sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==", + "license": "MIT", + "dependencies": { + "lodash.get": "^4.4.2", + "lodash.isequal": "^4.5.0", + "validator": "^13.7.0" + }, + "bin": { + "z-schema": "bin/z-schema" + }, + "engines": { + "node": ">=8.0.0" + }, + "optionalDependencies": { + "commander": "^9.4.1" + } + }, + "node_modules/z-schema/node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": "^12.20.0 || >=14" + } } } } diff --git a/api.hyungi.net/package.json b/api.hyungi.net/package.json index f45fe7c..745632b 100644 --- a/api.hyungi.net/package.json +++ b/api.hyungi.net/package.json @@ -21,6 +21,8 @@ "mysql2": "^3.14.1", "pm2": "^5.3.0", "qrcode": "^1.5.4", - "sqlite3": "^5.1.6" + "sqlite3": "^5.1.6", + "swagger-jsdoc": "^6.2.8", + "swagger-ui-express": "^5.0.1" } } diff --git a/api.hyungi.net/routes/authRoutes.js b/api.hyungi.net/routes/authRoutes.js index afaa9fd..a4bf573 100644 --- a/api.hyungi.net/routes/authRoutes.js +++ b/api.hyungi.net/routes/authRoutes.js @@ -1,3 +1,10 @@ +/** + * @swagger + * tags: + * name: Authentication + * description: 사용자 인증 및 권한 관리 API + */ + // routes/authRoutes.js - 비밀번호 변경 및 보안 기능 포함 완전판 const express = require('express'); const bcrypt = require('bcryptjs'); @@ -71,7 +78,49 @@ const recordLoginHistory = async (connection, userId, success, ipAddress, userAg }; /** - * 로그인 - DB 연동 (보안 강화) + * @swagger + * /api/auth/login: + * post: + * tags: [Authentication] + * summary: 사용자 로그인 + * description: 사용자명과 비밀번호로 로그인하여 JWT 토큰을 발급받습니다. + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/LoginRequest' + * responses: + * 200: + * description: 로그인 성공 + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/LoginResponse' + * 400: + * description: 잘못된 요청 (사용자명 또는 비밀번호 누락) + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * 401: + * description: 인증 실패 (잘못된 사용자명 또는 비밀번호) + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * 429: + * description: 너무 많은 로그인 시도 + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' + * 500: + * description: 서버 내부 오류 + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ErrorResponse' */ router.post('/login', authController.login); diff --git a/api.hyungi.net/routes/workerRoutes.js b/api.hyungi.net/routes/workerRoutes.js index 72324ef..15d5f0e 100644 --- a/api.hyungi.net/routes/workerRoutes.js +++ b/api.hyungi.net/routes/workerRoutes.js @@ -1,20 +1,228 @@ +/** + * @swagger + * tags: + * name: Workers + * description: 작업자 관리 API + */ + const express = require('express'); const router = express.Router(); const workerController = require('../controllers/workerController'); -// 작업자 생성 +/** + * @swagger + * /api/workers: + * post: + * tags: [Workers] + * summary: 작업자 생성 + * description: 새로운 작업자를 생성합니다. + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - worker_name + * properties: + * worker_name: + * type: string + * example: "김철수" + * position: + * type: string + * example: "용접공" + * department: + * type: string + * example: "생산부" + * phone: + * type: string + * example: "010-1234-5678" + * email: + * type: string + * format: email + * example: "worker@technicalkorea.com" + * responses: + * 201: + * description: 작업자 생성 성공 + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/SuccessResponse' + * 400: + * description: 잘못된 요청 + * 401: + * description: 인증 필요 + * 500: + * description: 서버 오류 + * get: + * tags: [Workers] + * summary: 전체 작업자 조회 + * description: 모든 작업자 목록을 조회합니다. + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: 작업자 목록 조회 성공 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * message: + * type: string + * example: "작업자 목록 조회 성공" + * data: + * type: array + * items: + * $ref: '#/components/schemas/Worker' + * meta: + * type: object + * properties: + * count: + * type: integer + * example: 10 + * 401: + * description: 인증 필요 + * 500: + * description: 서버 오류 + */ router.post('/', workerController.createWorker); - -// 전체 작업자 조회 router.get('/', workerController.getAllWorkers); -// 특정 작업자 조회 +/** + * @swagger + * /api/workers/{worker_id}: + * get: + * tags: [Workers] + * summary: 특정 작업자 조회 + * description: ID로 특정 작업자 정보를 조회합니다. + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: worker_id + * required: true + * schema: + * type: integer + * description: 작업자 ID + * responses: + * 200: + * description: 작업자 조회 성공 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * message: + * type: string + * example: "작업자 조회 성공" + * data: + * $ref: '#/components/schemas/Worker' + * 400: + * description: 잘못된 작업자 ID + * 401: + * description: 인증 필요 + * 404: + * description: 작업자를 찾을 수 없음 + * 500: + * description: 서버 오류 + * put: + * tags: [Workers] + * summary: 작업자 정보 수정 + * description: 작업자 정보를 수정합니다. + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: worker_id + * required: true + * schema: + * type: integer + * description: 작업자 ID + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * worker_name: + * type: string + * example: "김철수" + * position: + * type: string + * example: "용접공" + * department: + * type: string + * example: "생산부" + * phone: + * type: string + * example: "010-1234-5678" + * email: + * type: string + * format: email + * example: "worker@technicalkorea.com" + * responses: + * 200: + * description: 작업자 수정 성공 + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/SuccessResponse' + * 400: + * description: 잘못된 요청 + * 401: + * description: 인증 필요 + * 404: + * description: 작업자를 찾을 수 없음 + * 500: + * description: 서버 오류 + * delete: + * tags: [Workers] + * summary: 작업자 삭제 + * description: 작업자를 삭제합니다. + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: worker_id + * required: true + * schema: + * type: integer + * description: 작업자 ID + * responses: + * 200: + * description: 작업자 삭제 성공 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * message: + * type: string + * example: "작업자가 성공적으로 삭제되었습니다." + * 400: + * description: 잘못된 작업자 ID + * 401: + * description: 인증 필요 + * 404: + * description: 작업자를 찾을 수 없음 + * 500: + * description: 서버 오류 + */ router.get('/:worker_id', workerController.getWorkerById); - -// 작업자 업데이트 router.put('/:worker_id', workerController.updateWorker); - -// 작업자 삭제 router.delete('/:worker_id', workerController.removeWorker); module.exports = router; \ No newline at end of file