refactor: 로그인 API의 DB 스키마 및 구조 개선
- 새로운 DB 스키마(v2) 추가 (테이블명 snake_case, FK 적용) - 룰.md에 API 성능 관리 규칙 추가 - 로그인 관련 로직을 새로운 스키마에 맞게 수정 - Service와 Model의 역할 분리를 명확하게 리팩토링
This commit is contained in:
@@ -1,56 +1,29 @@
|
|||||||
const { getDb } = require('../dbPool');
|
const { getDb } = require('../dbPool');
|
||||||
const bcrypt = require('bcryptjs');
|
const bcrypt = require('bcryptjs');
|
||||||
const jwt = require('jsonwebtoken');
|
const jwt = require('jsonwebtoken');
|
||||||
|
const authService = require('../services/auth.service');
|
||||||
|
|
||||||
exports.login = async (req, res) => {
|
const login = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { username, password } = req.body;
|
const { username, password } = req.body;
|
||||||
const db = await getDb();
|
const ipAddress = req.ip || req.connection.remoteAddress;
|
||||||
|
const userAgent = req.headers['user-agent'];
|
||||||
|
|
||||||
const [rows] = await db.query(
|
if (!username || !password) {
|
||||||
'SELECT * FROM Users WHERE username = ?',
|
return res.status(400).json({ error: '사용자명과 비밀번호를 입력해주세요.' });
|
||||||
[username]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (rows.length === 0) {
|
|
||||||
return res.status(401).json({ error: '존재하지 않는 사용자입니다.' });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = rows[0];
|
const result = await authService.loginService(username, password, ipAddress, userAgent);
|
||||||
const isMatch = await bcrypt.compare(password, user.password);
|
|
||||||
if (!isMatch) {
|
if (!result.success) {
|
||||||
return res.status(401).json({ error: '비밀번호가 일치하지 않습니다.' });
|
return res.status(result.status || 400).json({ error: result.error });
|
||||||
}
|
}
|
||||||
|
|
||||||
// JWT 토큰 생성
|
res.json(result.data);
|
||||||
const token = jwt.sign(
|
|
||||||
{
|
|
||||||
user_id: user.user_id,
|
|
||||||
username: user.username,
|
|
||||||
name: user.name,
|
|
||||||
role: user.role,
|
|
||||||
access_level: user.access_level,
|
|
||||||
worker_id: user.worker_id
|
|
||||||
},
|
|
||||||
process.env.JWT_SECRET,
|
|
||||||
{ expiresIn: '1d' }
|
|
||||||
);
|
|
||||||
|
|
||||||
// 토큰 포함 응답
|
} catch (error) {
|
||||||
return res.status(200).json({
|
console.error('Login controller error:', error);
|
||||||
success: true,
|
res.status(500).json({ error: error.message || '서버 오류가 발생했습니다.' });
|
||||||
token,
|
|
||||||
user_id: user.user_id,
|
|
||||||
username: user.username,
|
|
||||||
role: user.role
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
console.error('[로그인 오류]', err);
|
|
||||||
return res.status(500).json({
|
|
||||||
error: '서버 내부 오류',
|
|
||||||
detail: err.message || String(err)
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -175,4 +148,8 @@ exports.getAllUsers = async (req, res) => {
|
|||||||
console.error('[사용자 목록 조회 실패]', err);
|
console.error('[사용자 목록 조회 실패]', err);
|
||||||
res.status(500).json({ error: '서버 오류' });
|
res.status(500).json({ error: '서버 오류' });
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
login
|
||||||
};
|
};
|
||||||
297
api.hyungi.net/hyungi_schema_v2.sql
Normal file
297
api.hyungi.net/hyungi_schema_v2.sql
Normal file
@@ -0,0 +1,297 @@
|
|||||||
|
-- Hyungi Technical Korea - Database Schema v2
|
||||||
|
-- 개선 사항:
|
||||||
|
-- 1. 모든 테이블 및 컬럼 이름을 snake_case로 통일
|
||||||
|
-- 2. 데이터 무결성 강화를 위한 외래 키(Foreign Key) 제약조건 추가
|
||||||
|
-- 3. 유사/중복 테이블 통합 및 정리 제안 (예: tasks, work_types)
|
||||||
|
-- 4. 컬럼의 기본값(Default), 코멘트 등 명확화
|
||||||
|
|
||||||
|
SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
|
||||||
|
START TRANSACTION;
|
||||||
|
SET time_zone = "+09:00";
|
||||||
|
|
||||||
|
-- =================================================================
|
||||||
|
-- 1. 인증 및 사용자 관련 (Auth Domain)
|
||||||
|
-- =================================================================
|
||||||
|
|
||||||
|
-- 사용자 계정 정보
|
||||||
|
CREATE TABLE `users` (
|
||||||
|
`user_id` int(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
`username` varchar(100) NOT NULL,
|
||||||
|
`password` varchar(255) NOT NULL,
|
||||||
|
`name` varchar(50) DEFAULT NULL,
|
||||||
|
`email` varchar(255) DEFAULT NULL,
|
||||||
|
`role` varchar(30) DEFAULT 'user' COMMENT '역할 (system, admin, leader, user)',
|
||||||
|
`access_level` varchar(30) DEFAULT NULL COMMENT '접근 레벨 (레거시 필드, role로 통합 고려)',
|
||||||
|
`worker_id` int(11) DEFAULT NULL COMMENT '연결된 작업자 ID',
|
||||||
|
`is_active` tinyint(1) DEFAULT 1 COMMENT '계정 활성화 여부',
|
||||||
|
`last_login_at` datetime DEFAULT NULL COMMENT '마지막 로그인 시간',
|
||||||
|
`password_changed_at` datetime DEFAULT NULL,
|
||||||
|
`failed_login_attempts` int(11) DEFAULT 0,
|
||||||
|
`locked_until` datetime DEFAULT NULL,
|
||||||
|
`created_at` timestamp NULL DEFAULT current_timestamp(),
|
||||||
|
`updated_at` timestamp NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
|
||||||
|
PRIMARY KEY (`user_id`),
|
||||||
|
UNIQUE KEY `username` (`username`),
|
||||||
|
KEY `fk_users_worker_id` (`worker_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- 로그인 이력
|
||||||
|
CREATE TABLE `login_logs` (
|
||||||
|
`log_id` int(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
`user_id` int(11) DEFAULT NULL,
|
||||||
|
`login_time` datetime DEFAULT current_timestamp(),
|
||||||
|
`ip_address` varchar(45) DEFAULT NULL,
|
||||||
|
`user_agent` text DEFAULT NULL,
|
||||||
|
`login_status` enum('success','failed','locked') DEFAULT 'success',
|
||||||
|
`failure_reason` varchar(100) DEFAULT NULL,
|
||||||
|
PRIMARY KEY (`log_id`),
|
||||||
|
KEY `fk_login_logs_user_id` (`user_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- 비밀번호 변경 이력
|
||||||
|
CREATE TABLE `password_change_logs` (
|
||||||
|
`log_id` int(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
`user_id` int(11) NOT NULL,
|
||||||
|
`changed_by_user_id` int(11) DEFAULT NULL COMMENT '누가 변경했는지 (관리자)',
|
||||||
|
`changed_at` datetime DEFAULT current_timestamp(),
|
||||||
|
`change_type` enum('self','admin','reset','initial') DEFAULT 'self',
|
||||||
|
`ip_address` varchar(45) DEFAULT NULL,
|
||||||
|
PRIMARY KEY (`log_id`),
|
||||||
|
KEY `fk_pw_change_user_id` (`user_id`),
|
||||||
|
KEY `fk_pw_change_changed_by` (`changed_by_user_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
|
||||||
|
-- =================================================================
|
||||||
|
-- 2. 기초 정보 (Master Data)
|
||||||
|
-- =================================================================
|
||||||
|
|
||||||
|
-- 프로젝트 정보
|
||||||
|
CREATE TABLE `projects` (
|
||||||
|
`project_id` int(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
`job_no` varchar(50) NOT NULL,
|
||||||
|
`project_name` varchar(255) NOT NULL,
|
||||||
|
`contract_date` date DEFAULT NULL,
|
||||||
|
`due_date` date DEFAULT NULL,
|
||||||
|
`delivery_method` varchar(100) DEFAULT NULL,
|
||||||
|
`site` varchar(100) DEFAULT NULL,
|
||||||
|
`pm` varchar(100) DEFAULT NULL,
|
||||||
|
`created_at` timestamp NULL DEFAULT current_timestamp(),
|
||||||
|
`updated_at` timestamp NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
|
||||||
|
PRIMARY KEY (`project_id`),
|
||||||
|
UNIQUE KEY `job_no` (`job_no`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- 작업자(인력) 정보
|
||||||
|
CREATE TABLE `workers` (
|
||||||
|
`worker_id` int(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
`worker_name` varchar(100) NOT NULL,
|
||||||
|
`job_type` varchar(100) DEFAULT NULL COMMENT '직종',
|
||||||
|
`join_date` date DEFAULT NULL COMMENT '입사일',
|
||||||
|
`status` varchar(20) DEFAULT 'active' COMMENT '상태 (active, inactive)',
|
||||||
|
`created_at` timestamp NULL DEFAULT current_timestamp(),
|
||||||
|
`updated_at` timestamp NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
|
||||||
|
PRIMARY KEY (`worker_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- 작업자 그룹 (팀)
|
||||||
|
CREATE TABLE `worker_groups` (
|
||||||
|
`group_id` int(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
`group_name` varchar(100) DEFAULT NULL,
|
||||||
|
`group_leader_id` int(11) NOT NULL COMMENT '그룹장 user_id',
|
||||||
|
`worker_id` int(11) NOT NULL COMMENT '소속 작업자 worker_id',
|
||||||
|
`is_active` tinyint(1) DEFAULT 1,
|
||||||
|
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
|
||||||
|
`updated_at` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
|
||||||
|
PRIMARY KEY (`group_id`),
|
||||||
|
KEY `fk_w_groups_leader_id` (`group_leader_id`),
|
||||||
|
KEY `fk_w_groups_worker_id` (`worker_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- 표준 작업(공수) 종류 (기존 Tasks 와 work_types 통합 제안)
|
||||||
|
CREATE TABLE `tasks` (
|
||||||
|
`task_id` int(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
`task_category` varchar(255) NOT NULL COMMENT '작업 대분류 (예: PKG, Vessel)',
|
||||||
|
`task_subcategory` varchar(255) DEFAULT NULL COMMENT '작업 중분류 (예: Pipe Pre-Fabrication)',
|
||||||
|
`task_name` varchar(255) NOT NULL COMMENT '실제 작업명 (예: 취부&용접)',
|
||||||
|
`description` text DEFAULT NULL,
|
||||||
|
`is_active` tinyint(1) DEFAULT 1,
|
||||||
|
`created_at` timestamp NULL DEFAULT current_timestamp(),
|
||||||
|
`updated_at` timestamp NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
|
||||||
|
PRIMARY KEY (`task_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- 파이프 사양(Spec) 정보
|
||||||
|
CREATE TABLE `pipe_specs` (
|
||||||
|
`spec_id` int(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
`material` varchar(50) NOT NULL COMMENT '재질 (예: SS400, STS304)',
|
||||||
|
`diameter_in` varchar(10) NOT NULL COMMENT '직경 (inch, 예: 2")',
|
||||||
|
`schedule` varchar(50) NOT NULL COMMENT '스케줄 (예: STD, SCH10, SCH40)',
|
||||||
|
PRIMARY KEY (`spec_id`),
|
||||||
|
UNIQUE KEY `uk_pipe_specs` (`material`,`diameter_in`,`schedule`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||||
|
|
||||||
|
-- 공장 설비/장비 목록
|
||||||
|
CREATE TABLE `equipment_list` (
|
||||||
|
`equipment_id` int(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
`factory_id` int(11) DEFAULT NULL COMMENT '소속 공장 ID',
|
||||||
|
`equipment_name` varchar(255) NOT NULL,
|
||||||
|
`model` varchar(100) DEFAULT NULL,
|
||||||
|
`status` varchar(50) DEFAULT 'operational',
|
||||||
|
`purchase_date` date DEFAULT NULL,
|
||||||
|
`description` text DEFAULT NULL,
|
||||||
|
`created_at` timestamp NULL DEFAULT current_timestamp(),
|
||||||
|
`updated_at` timestamp NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
|
||||||
|
PRIMARY KEY (`equipment_id`),
|
||||||
|
KEY `fk_equip_factory_id` (`factory_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- 공장 구역 정보
|
||||||
|
CREATE TABLE `factory_info` (
|
||||||
|
`factory_id` int(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
`factory_name` varchar(255) NOT NULL,
|
||||||
|
`address` varchar(255) DEFAULT NULL,
|
||||||
|
`description` text DEFAULT NULL,
|
||||||
|
`map_image_url` varchar(255) DEFAULT NULL,
|
||||||
|
`created_at` timestamp NULL DEFAULT current_timestamp(),
|
||||||
|
`updated_at` timestamp NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
|
||||||
|
PRIMARY KEY (`factory_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- 코드 정의 (기존 IssueTypes, error_types 등을 통합 관리)
|
||||||
|
CREATE TABLE `code_types` (
|
||||||
|
`code_type_id` VARCHAR(50) NOT NULL COMMENT '코드 타입 ID (예: ISSUE_TYPE, ERROR_TYPE)',
|
||||||
|
`code_type_name` VARCHAR(100) NOT NULL COMMENT '코드 타입명 (예: 이슈 유형, 에러 유형)',
|
||||||
|
`description` TEXT,
|
||||||
|
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`code_type_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE `codes` (
|
||||||
|
`code_id` INT NOT NULL AUTO_INCREMENT,
|
||||||
|
`code_type_id` VARCHAR(50) NOT NULL COMMENT '참조하는 코드 타입 ID',
|
||||||
|
`code_value` VARCHAR(100) NOT NULL COMMENT '코드 값 (예: design_miss)',
|
||||||
|
`code_name` VARCHAR(100) NOT NULL COMMENT '코드 표시명 (예: 설계 미스)',
|
||||||
|
`code_order` INT DEFAULT 0 COMMENT '정렬 순서',
|
||||||
|
`is_active` TINYINT(1) DEFAULT 1,
|
||||||
|
`description` TEXT,
|
||||||
|
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`code_id`),
|
||||||
|
UNIQUE KEY `uk_code` (`code_type_id`, `code_value`),
|
||||||
|
KEY `fk_codes_code_type_id` (`code_type_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
|
||||||
|
-- =================================================================
|
||||||
|
-- 3. 업무 데이터 (Transactional Data)
|
||||||
|
-- =================================================================
|
||||||
|
|
||||||
|
-- 일일 작업 보고
|
||||||
|
CREATE TABLE `daily_work_reports` (
|
||||||
|
`report_id` int(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
`report_date` date NOT NULL COMMENT '작업 날짜',
|
||||||
|
`worker_id` int(11) NOT NULL COMMENT '작업자 ID',
|
||||||
|
`project_id` int(11) NOT NULL COMMENT '프로젝트 ID',
|
||||||
|
`task_id` int(11) NOT NULL COMMENT '작업 ID',
|
||||||
|
`work_hours` decimal(4,2) NOT NULL COMMENT '작업 시간',
|
||||||
|
`is_error` tinyint(1) NOT NULL DEFAULT 0 COMMENT '에러 여부 (0: 정상, 1: 에러)',
|
||||||
|
`error_type_code_id` int(11) DEFAULT NULL COMMENT '에러 유형 코드 ID (codes 테이블 참조)',
|
||||||
|
`created_by_user_id` int(11) NOT NULL COMMENT '작성자 user_id',
|
||||||
|
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
|
||||||
|
`updated_at` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
|
||||||
|
PRIMARY KEY (`report_id`),
|
||||||
|
KEY `fk_dwr_worker_id` (`worker_id`),
|
||||||
|
KEY `fk_dwr_project_id` (`project_id`),
|
||||||
|
KEY `fk_dwr_task_id` (`task_id`),
|
||||||
|
KEY `fk_dwr_error_type` (`error_type_code_id`),
|
||||||
|
KEY `fk_dwr_created_by` (`created_by_user_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- 일일 이슈 보고
|
||||||
|
CREATE TABLE `daily_issue_reports` (
|
||||||
|
`issue_report_id` int(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
`report_date` date NOT NULL,
|
||||||
|
`worker_id` int(11) NOT NULL,
|
||||||
|
`project_id` int(11) NOT NULL,
|
||||||
|
`issue_type_code_id` int(11) DEFAULT NULL COMMENT '이슈 유형 코드 ID (codes 테이블 참조)',
|
||||||
|
`description` text DEFAULT NULL,
|
||||||
|
`start_time` time NOT NULL,
|
||||||
|
`end_time` time NOT NULL,
|
||||||
|
`created_at` timestamp NULL DEFAULT current_timestamp(),
|
||||||
|
PRIMARY KEY (`issue_report_id`),
|
||||||
|
KEY `fk_dir_worker_id` (`worker_id`),
|
||||||
|
KEY `fk_dir_project_id` (`project_id`),
|
||||||
|
KEY `fk_dir_issue_type` (`issue_type_code_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||||
|
|
||||||
|
-- 절단 계획
|
||||||
|
CREATE TABLE `cutting_plans` (
|
||||||
|
`cutting_plan_id` int(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
`project_id` int(11) NOT NULL,
|
||||||
|
`spec_id` int(11) NOT NULL COMMENT '파이프 사양 ID',
|
||||||
|
`drawing_name` varchar(255) NOT NULL,
|
||||||
|
`area_number` varchar(100) DEFAULT NULL,
|
||||||
|
`spool_number` varchar(255) DEFAULT NULL,
|
||||||
|
`length` decimal(10,2) DEFAULT NULL,
|
||||||
|
`created_at` timestamp NULL DEFAULT current_timestamp(),
|
||||||
|
`updated_at` timestamp NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
|
||||||
|
PRIMARY KEY (`cutting_plan_id`),
|
||||||
|
KEY `fk_cp_project_id` (`project_id`),
|
||||||
|
KEY `fk_cp_spec_id` (`spec_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- =================================================================
|
||||||
|
-- 외래 키(Foreign Key) 제약조건 설정
|
||||||
|
-- =================================================================
|
||||||
|
|
||||||
|
-- Auth Domain
|
||||||
|
ALTER TABLE `users`
|
||||||
|
ADD CONSTRAINT `fk_users_worker_id` FOREIGN KEY (`worker_id`) REFERENCES `workers` (`worker_id`) ON DELETE SET NULL;
|
||||||
|
ALTER TABLE `login_logs`
|
||||||
|
ADD CONSTRAINT `fk_login_logs_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`user_id`) ON DELETE SET NULL;
|
||||||
|
ALTER TABLE `password_change_logs`
|
||||||
|
ADD CONSTRAINT `fk_pw_change_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`user_id`) ON DELETE CASCADE,
|
||||||
|
ADD CONSTRAINT `fk_pw_change_changed_by` FOREIGN KEY (`changed_by_user_id`) REFERENCES `users` (`user_id`) ON DELETE SET NULL;
|
||||||
|
|
||||||
|
-- Master Data
|
||||||
|
ALTER TABLE `worker_groups`
|
||||||
|
ADD CONSTRAINT `fk_w_groups_leader_id` FOREIGN KEY (`group_leader_id`) REFERENCES `users` (`user_id`) ON DELETE CASCADE,
|
||||||
|
ADD CONSTRAINT `fk_w_groups_worker_id` FOREIGN KEY (`worker_id`) REFERENCES `workers` (`worker_id`) ON DELETE CASCADE;
|
||||||
|
ALTER TABLE `equipment_list`
|
||||||
|
ADD CONSTRAINT `fk_equip_factory_id` FOREIGN KEY (`factory_id`) REFERENCES `factory_info` (`factory_id`) ON DELETE SET NULL;
|
||||||
|
ALTER TABLE `codes`
|
||||||
|
ADD CONSTRAINT `fk_codes_code_type_id` FOREIGN KEY (`code_type_id`) REFERENCES `code_types` (`code_type_id`) ON DELETE CASCADE;
|
||||||
|
|
||||||
|
-- Transactional Data
|
||||||
|
ALTER TABLE `daily_work_reports`
|
||||||
|
ADD CONSTRAINT `fk_dwr_worker_id` FOREIGN KEY (`worker_id`) REFERENCES `workers` (`worker_id`) ON DELETE RESTRICT,
|
||||||
|
ADD CONSTRAINT `fk_dwr_project_id` FOREIGN KEY (`project_id`) REFERENCES `projects` (`project_id`) ON DELETE RESTRICT,
|
||||||
|
ADD CONSTRAINT `fk_dwr_task_id` FOREIGN KEY (`task_id`) REFERENCES `tasks` (`task_id`) ON DELETE RESTRICT,
|
||||||
|
ADD CONSTRAINT `fk_dwr_error_type` FOREIGN KEY (`error_type_code_id`) REFERENCES `codes` (`code_id`) ON DELETE SET NULL,
|
||||||
|
ADD CONSTRAINT `fk_dwr_created_by` FOREIGN KEY (`created_by_user_id`) REFERENCES `users` (`user_id`) ON DELETE RESTRICT;
|
||||||
|
ALTER TABLE `daily_issue_reports`
|
||||||
|
ADD CONSTRAINT `fk_dir_worker_id` FOREIGN KEY (`worker_id`) REFERENCES `workers` (`worker_id`) ON DELETE RESTRICT,
|
||||||
|
ADD CONSTRAINT `fk_dir_project_id` FOREIGN KEY (`project_id`) REFERENCES `projects` (`project_id`) ON DELETE RESTRICT,
|
||||||
|
ADD CONSTRAINT `fk_dir_issue_type` FOREIGN KEY (`issue_type_code_id`) REFERENCES `codes` (`code_id`) ON DELETE SET NULL;
|
||||||
|
ALTER TABLE `cutting_plans`
|
||||||
|
ADD CONSTRAINT `fk_cp_project_id` FOREIGN KEY (`project_id`) REFERENCES `projects` (`project_id`) ON DELETE CASCADE,
|
||||||
|
ADD CONSTRAINT `fk_cp_spec_id` FOREIGN KEY (`spec_id`) REFERENCES `pipe_specs` (`spec_id`) ON DELETE RESTRICT;
|
||||||
|
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
|
|
||||||
|
-- =================================================================
|
||||||
|
-- 기존 테이블 (Legacy) - 검토 후 마이그레이션 및 삭제 필요
|
||||||
|
-- =================================================================
|
||||||
|
/*
|
||||||
|
-- 기존 Tasks, IssueTypes, error_types 등은 `codes` 와 `code_types`로 통합 제안
|
||||||
|
-- 아래 테이블들은 `codes` 테이블로 데이터 마이그레이션 후 삭제 고려
|
||||||
|
CREATE TABLE `IssueTypes` ( ... );
|
||||||
|
CREATE TABLE `error_types` ( ... );
|
||||||
|
CREATE TABLE `work_status_types` ( ... );
|
||||||
|
CREATE TABLE `work_types` ( ... );
|
||||||
|
|
||||||
|
-- daily_work_reports 로 통합된 것으로 추정되는 레거시 테이블
|
||||||
|
CREATE TABLE `WorkReports` ( ... );
|
||||||
|
CREATE TABLE `daily_worker_summary` ( ... );
|
||||||
|
*/
|
||||||
@@ -1,20 +1,87 @@
|
|||||||
const { getDb } = require('../dbPool');
|
const dbPool = require('../dbPool');
|
||||||
|
|
||||||
// 사용자 조회
|
// 사용자 조회
|
||||||
const findByUsername = async (username) => {
|
const findByUsername = async (username) => {
|
||||||
|
let connection;
|
||||||
try {
|
try {
|
||||||
const db = await getDb();
|
connection = await dbPool.getConnection();
|
||||||
const [rows] = await db.query(
|
const [rows] = await connection.query(
|
||||||
'SELECT * FROM Users WHERE username = ?', [username]
|
'SELECT * FROM users WHERE username = ?', [username]
|
||||||
);
|
);
|
||||||
return rows[0];
|
return rows[0];
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('DB 오류 - 사용자 조회 실패:', err);
|
console.error('DB 오류 - 사용자 조회 실패:', err);
|
||||||
throw err;
|
throw err;
|
||||||
|
} finally {
|
||||||
|
if (connection) connection.release();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 로그인 실패 횟수를 1 증가시킵니다.
|
||||||
|
* @param {number} userId - 사용자 ID
|
||||||
|
*/
|
||||||
|
const incrementFailedLoginAttempts = async (userId) => {
|
||||||
|
let connection;
|
||||||
|
try {
|
||||||
|
connection = await dbPool.getConnection();
|
||||||
|
await connection.execute(
|
||||||
|
'UPDATE users SET failed_login_attempts = failed_login_attempts + 1 WHERE user_id = ?',
|
||||||
|
[userId]
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('DB 오류 - 로그인 실패 횟수 증가 실패:', err);
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
if (connection) connection.release();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 특정 사용자의 계정을 잠급니다.
|
||||||
|
* @param {number} userId - 사용자 ID
|
||||||
|
*/
|
||||||
|
const lockUserAccount = async (userId) => {
|
||||||
|
let connection;
|
||||||
|
try {
|
||||||
|
connection = await dbPool.getConnection();
|
||||||
|
await connection.execute(
|
||||||
|
'UPDATE users SET locked_until = DATE_ADD(NOW(), INTERVAL 15 MINUTE) WHERE user_id = ?',
|
||||||
|
[userId]
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('DB 오류 - 계정 잠금 실패:', err);
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
if (connection) connection.release();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 로그인 성공 시, 마지막 로그인 시간을 업데이트하고 실패 횟수와 잠금 상태를 초기화합니다.
|
||||||
|
* @param {number} userId - 사용자 ID
|
||||||
|
*/
|
||||||
|
const resetLoginAttempts = async (userId) => {
|
||||||
|
let connection;
|
||||||
|
try {
|
||||||
|
connection = await dbPool.getConnection();
|
||||||
|
await connection.execute(
|
||||||
|
'UPDATE users SET last_login_at = NOW(), failed_login_attempts = 0, locked_until = NULL WHERE user_id = ?',
|
||||||
|
[userId]
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('DB 오류 - 로그인 상태 초기화 실패:', err);
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
if (connection) connection.release();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
// 명확한 내보내기
|
// 명확한 내보내기
|
||||||
module.exports = {
|
module.exports = {
|
||||||
findByUsername
|
findByUsername,
|
||||||
|
incrementFailedLoginAttempts,
|
||||||
|
lockUserAccount,
|
||||||
|
resetLoginAttempts
|
||||||
};
|
};
|
||||||
@@ -5,6 +5,7 @@ const jwt = require('jsonwebtoken');
|
|||||||
const mysql = require('mysql2/promise');
|
const mysql = require('mysql2/promise');
|
||||||
const { verifyToken } = require('../middlewares/authMiddleware');
|
const { verifyToken } = require('../middlewares/authMiddleware');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
const authController = require('../controllers/authController');
|
||||||
|
|
||||||
// DB 연결 설정
|
// DB 연결 설정
|
||||||
const dbConfig = {
|
const dbConfig = {
|
||||||
@@ -72,133 +73,7 @@ const recordLoginHistory = async (connection, userId, success, ipAddress, userAg
|
|||||||
/**
|
/**
|
||||||
* 로그인 - DB 연동 (보안 강화)
|
* 로그인 - DB 연동 (보안 강화)
|
||||||
*/
|
*/
|
||||||
router.post('/login', checkLoginAttempts, async (req, res) => {
|
router.post('/login', authController.login);
|
||||||
let connection;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { username, password } = req.body;
|
|
||||||
const ipAddress = req.ip || req.connection.remoteAddress;
|
|
||||||
const userAgent = req.headers['user-agent'];
|
|
||||||
|
|
||||||
console.log(`[로그인 시도] 사용자: ${username}, IP: ${ipAddress}`);
|
|
||||||
|
|
||||||
if (!username || !password) {
|
|
||||||
return res.status(400).json({ error: '사용자명과 비밀번호를 입력해주세요.' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// DB 연결
|
|
||||||
connection = await mysql.createConnection(dbConfig);
|
|
||||||
|
|
||||||
// 사용자 조회
|
|
||||||
const [rows] = await connection.execute(
|
|
||||||
'SELECT * FROM Users WHERE username = ?',
|
|
||||||
[username]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (rows.length === 0) {
|
|
||||||
console.log(`[로그인 실패] 사용자를 찾을 수 없음: ${username}`);
|
|
||||||
recordLoginAttempt(username, false);
|
|
||||||
// 보안상 구체적인 오류 메시지는 피함
|
|
||||||
return res.status(401).json({ error: '아이디 또는 비밀번호가 올바르지 않습니다.' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = rows[0];
|
|
||||||
|
|
||||||
// 계정 활성화 상태 확인
|
|
||||||
if (user.is_active === false) {
|
|
||||||
await recordLoginHistory(connection, user.user_id, false, ipAddress, userAgent, 'account_disabled');
|
|
||||||
return res.status(403).json({ error: '비활성화된 계정입니다. 관리자에게 문의하세요.' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// 계정 잠금 확인
|
|
||||||
if (user.locked_until && new Date(user.locked_until) > new Date()) {
|
|
||||||
const remainingTime = Math.ceil((new Date(user.locked_until) - new Date()) / 1000 / 60);
|
|
||||||
return res.status(429).json({ error: `계정이 잠겨있습니다. ${remainingTime}분 후에 다시 시도하세요.` });
|
|
||||||
}
|
|
||||||
|
|
||||||
// 비밀번호 확인
|
|
||||||
const isValid = await bcrypt.compare(password, user.password);
|
|
||||||
if (!isValid) {
|
|
||||||
console.log(`[로그인 실패] 비밀번호 불일치: ${username}`);
|
|
||||||
recordLoginAttempt(username, false);
|
|
||||||
|
|
||||||
// 실패 횟수 업데이트
|
|
||||||
await connection.execute(
|
|
||||||
'UPDATE Users SET failed_login_attempts = failed_login_attempts + 1 WHERE user_id = ?',
|
|
||||||
[user.user_id]
|
|
||||||
);
|
|
||||||
|
|
||||||
// 5회 실패 시 계정 잠금
|
|
||||||
if (user.failed_login_attempts >= 4) {
|
|
||||||
await connection.execute(
|
|
||||||
'UPDATE Users SET locked_until = DATE_ADD(NOW(), INTERVAL 15 MINUTE) WHERE user_id = ?',
|
|
||||||
[user.user_id]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await recordLoginHistory(connection, user.user_id, false, ipAddress, userAgent, 'invalid_password');
|
|
||||||
return res.status(401).json({ error: '아이디 또는 비밀번호가 올바르지 않습니다.' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// 로그인 성공
|
|
||||||
recordLoginAttempt(username, true);
|
|
||||||
|
|
||||||
// 마지막 로그인 시간 업데이트 및 실패 횟수 초기화
|
|
||||||
await connection.execute(
|
|
||||||
'UPDATE Users SET last_login_at = NOW(), failed_login_attempts = 0, locked_until = NULL WHERE user_id = ?',
|
|
||||||
[user.user_id]
|
|
||||||
);
|
|
||||||
|
|
||||||
// JWT 토큰 생성
|
|
||||||
const token = jwt.sign(
|
|
||||||
{
|
|
||||||
user_id: user.user_id,
|
|
||||||
username: user.username,
|
|
||||||
access_level: user.access_level,
|
|
||||||
worker_id: user.worker_id,
|
|
||||||
name: user.name || user.username
|
|
||||||
},
|
|
||||||
process.env.JWT_SECRET || 'your-secret-key',
|
|
||||||
{ expiresIn: process.env.JWT_EXPIRES_IN || '24h' }
|
|
||||||
);
|
|
||||||
|
|
||||||
// 리프레시 토큰 생성
|
|
||||||
const refreshToken = jwt.sign(
|
|
||||||
{
|
|
||||||
user_id: user.user_id,
|
|
||||||
type: 'refresh'
|
|
||||||
},
|
|
||||||
process.env.JWT_REFRESH_SECRET || process.env.JWT_SECRET || 'your-refresh-secret',
|
|
||||||
{ expiresIn: process.env.JWT_REFRESH_EXPIRES_IN || '7d' }
|
|
||||||
);
|
|
||||||
|
|
||||||
// 로그인 이력 기록
|
|
||||||
await recordLoginHistory(connection, user.user_id, true, ipAddress, userAgent);
|
|
||||||
|
|
||||||
console.log(`[로그인 성공] 사용자: ${user.username} (${user.access_level})`);
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
token,
|
|
||||||
refreshToken,
|
|
||||||
user: {
|
|
||||||
user_id: user.user_id,
|
|
||||||
username: user.username,
|
|
||||||
name: user.name || user.username,
|
|
||||||
access_level: user.access_level,
|
|
||||||
worker_id: user.worker_id
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Login error:', error);
|
|
||||||
res.status(500).json({ error: '서버 오류가 발생했습니다.' });
|
|
||||||
} finally {
|
|
||||||
if (connection) {
|
|
||||||
await connection.end();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 토큰 갱신
|
* 토큰 갱신
|
||||||
|
|||||||
100
api.hyungi.net/services/auth.service.js
Normal file
100
api.hyungi.net/services/auth.service.js
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
const bcrypt = require('bcryptjs');
|
||||||
|
const jwt = require('jsonwebtoken');
|
||||||
|
const userModel = require('../models/userModel');
|
||||||
|
const dbPool = require('../dbPool');
|
||||||
|
|
||||||
|
// 로그인 이력 기록 (서비스 내부 헬퍼 함수)
|
||||||
|
const recordLoginHistory = async (userId, success, ipAddress, userAgent, failureReason = null) => {
|
||||||
|
let connection;
|
||||||
|
try {
|
||||||
|
connection = await dbPool.getConnection();
|
||||||
|
await connection.execute(
|
||||||
|
`INSERT INTO login_logs (user_id, login_time, ip_address, user_agent, login_status, failure_reason)
|
||||||
|
VALUES (?, NOW(), ?, ?, ?, ?)`,
|
||||||
|
[userId, ipAddress || 'unknown', userAgent || 'unknown', success ? 'success' : 'failed', failureReason]
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('로그인 이력 기록 실패:', error);
|
||||||
|
} finally {
|
||||||
|
if (connection) connection.release();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const loginService = async (username, password, ipAddress, userAgent) => {
|
||||||
|
// 서비스 레이어에서는 더 이상 DB 커넥션을 직접 다루지 않음
|
||||||
|
try {
|
||||||
|
const user = await userModel.findByUsername(username);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
console.log(`[로그인 실패] 사용자를 찾을 수 없음: ${username}`);
|
||||||
|
return { success: false, status: 401, error: '아이디 또는 비밀번호가 올바르지 않습니다.' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.is_active === false) {
|
||||||
|
await recordLoginHistory(user.user_id, false, ipAddress, userAgent, 'account_disabled');
|
||||||
|
return { success: false, status: 403, error: '비활성화된 계정입니다. 관리자에게 문의하세요.' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.locked_until && new Date(user.locked_until) > new Date()) {
|
||||||
|
const remainingTime = Math.ceil((new Date(user.locked_until) - new Date()) / 1000 / 60);
|
||||||
|
return { success: false, status: 429, error: `계정이 잠겨있습니다. ${remainingTime}분 후에 다시 시도하세요.` };
|
||||||
|
}
|
||||||
|
|
||||||
|
const isValid = await bcrypt.compare(password, user.password);
|
||||||
|
if (!isValid) {
|
||||||
|
console.log(`[로그인 실패] 비밀번호 불일치: ${username}`);
|
||||||
|
|
||||||
|
// 모델 함수를 사용하여 로그인 실패 처리
|
||||||
|
await userModel.incrementFailedLoginAttempts(user.user_id);
|
||||||
|
|
||||||
|
if (user.failed_login_attempts >= 4) {
|
||||||
|
await userModel.lockUserAccount(user.user_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
await recordLoginHistory(user.user_id, false, ipAddress, userAgent, 'invalid_password');
|
||||||
|
return { success: false, status: 401, error: '아이디 또는 비밀번호가 올바르지 않습니다.' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 성공 시 모델 함수를 사용하여 상태 초기화
|
||||||
|
await userModel.resetLoginAttempts(user.user_id);
|
||||||
|
|
||||||
|
|
||||||
|
const token = jwt.sign(
|
||||||
|
{ user_id: user.user_id, username: user.username, access_level: user.access_level, worker_id: user.worker_id, name: user.name || user.username },
|
||||||
|
process.env.JWT_SECRET || 'your-secret-key',
|
||||||
|
{ expiresIn: process.env.JWT_EXPIRES_IN || '24h' }
|
||||||
|
);
|
||||||
|
|
||||||
|
const refreshToken = jwt.sign(
|
||||||
|
{ user_id: user.user_id, type: 'refresh' },
|
||||||
|
process.env.JWT_REFRESH_SECRET || process.env.JWT_SECRET || 'your-refresh-secret',
|
||||||
|
{ expiresIn: process.env.JWT_REFRESH_EXPIRES_IN || '7d' }
|
||||||
|
);
|
||||||
|
|
||||||
|
await recordLoginHistory(user.user_id, true, ipAddress, userAgent);
|
||||||
|
console.log(`[로그인 성공] 사용자: ${user.username} (${user.access_level})`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
token,
|
||||||
|
refreshToken,
|
||||||
|
user: {
|
||||||
|
user_id: user.user_id,
|
||||||
|
username: user.username,
|
||||||
|
name: user.name || user.username,
|
||||||
|
access_level: user.access_level,
|
||||||
|
worker_id: user.worker_id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Login service error:', error);
|
||||||
|
throw new Error('서버 오류가 발생했습니다.');
|
||||||
|
}
|
||||||
|
// 서비스 레이어에서는 더 이상 DB 커넥션을 직접 다루지 않음
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
loginService,
|
||||||
|
};
|
||||||
6
룰.md
6
룰.md
@@ -95,6 +95,12 @@
|
|||||||
- `404 Not Found`: 리소스 없음
|
- `404 Not Found`: 리소스 없음
|
||||||
- `500 Internal Server Error`: 서버 내부 오류
|
- `500 Internal Server Error`: 서버 내부 오류
|
||||||
|
|
||||||
|
### 4.4. 성능 및 자원 관리
|
||||||
|
|
||||||
|
- **최소한의 데이터 조회:** API는 반드시 필요한 데이터만 조회하고 반환해야 합니다. `SELECT *` 사용을 지양하고, 실제 클라이언트에서 사용하는 컬럼만 명시적으로 조회합니다.
|
||||||
|
- **효율적인 쿼리 작성:** 복잡한 `JOIN`이나 비효율적인 `WHERE` 조건으로 인해 데이터베이스에 과도한 부하를 주는 쿼리가 없는지 항상 확인합니다.
|
||||||
|
- **코드 리뷰:** 새로운 API를 개발하거나 기존 API를 수정할 때, 동료 개발자는 기능의 정확성뿐만 아니라 성능 측면(쿼리 효율, 불필요한 로직 등)도 함께 검토해야 합니다. 성능 저하가 의심되는 코드는 즉시 개선하는 것을 원칙으로 합니다.
|
||||||
|
|
||||||
## 5. 데이터베이스 관리
|
## 5. 데이터베이스 관리
|
||||||
|
|
||||||
- **테이블/컬럼 네이밍:** `snake_case` 사용
|
- **테이블/컬럼 네이밍:** `snake_case` 사용
|
||||||
|
|||||||
Reference in New Issue
Block a user