feat: Swagger/OpenAPI 문서화 시스템 구축

- Swagger 패키지 설치 및 설정:
  * swagger-jsdoc, swagger-ui-express 패키지 추가
  * /api-docs 엔드포인트에서 Swagger UI 제공
  * /api-docs.json 엔드포인트에서 JSON 스펙 제공

- 포괄적인 Swagger 설정 파일 생성:
  * config/swagger.js: OpenAPI 3.0 스펙 정의
  * 공통 스키마 정의 (User, Worker, Project, Task, DailyWorkReport)
  * 표준 응답 스키마 (SuccessResponse, ErrorResponse, PaginatedResponse)
  * JWT Bearer 인증 스키마 설정

- API 문서화 적용:
  * Authentication API: 로그인 엔드포인트 문서화
  * Workers API: 전체 CRUD 작업 문서화
  * 상세한 요청/응답 스키마 및 예시 포함
  * 에러 코드별 응답 정의

- Swagger UI 커스터마이징:
  * 브랜딩 및 UI 개선
  * 인증 토큰 지속성 설정
  * 필터링 및 탐색 기능 활성화

- 접근 방법:
  * http://localhost:20005/api-docs - Swagger UI
  * http://localhost:20005/api-docs.json - JSON 스펙
This commit is contained in:
Hyungi Ahn
2025-11-03 11:00:45 +09:00
parent 775cd12a25
commit dea325739a
6 changed files with 1030 additions and 21 deletions

View File

@@ -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;

View File

@@ -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);

View File

@@ -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"
}
}
}
}

View File

@@ -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"
}
}

View File

@@ -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);

View File

@@ -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;