refactor: System1 API 인증 체계 SSO 전환 및 마이그레이션 정비
- SSO JWT 인증으로 전환 (auth.service.js) - worker_id → user_id 마이그레이션 완료 - departments 연동, CORS 미들웨어 정리 - 불필요 파일 삭제 (tk_database.db, visitRequestController.js) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -7,8 +7,11 @@ WORKDIR /usr/src/app
|
||||
# 패키지 파일 복사 (캐싱 최적화)
|
||||
COPY package*.json ./
|
||||
|
||||
# 프로덕션 의존성만 설치
|
||||
RUN npm install --omit=dev
|
||||
# 프로덕션 의존성만 설치 (sharp용 빌드 도구 포함)
|
||||
RUN apk add --no-cache --virtual .build-deps python3 make g++ && \
|
||||
npm install --omit=dev && \
|
||||
npm install sharp && \
|
||||
apk del .build-deps
|
||||
|
||||
# 앱 소스 복사
|
||||
COPY . .
|
||||
|
||||
@@ -64,12 +64,7 @@ function setupMiddlewares(app) {
|
||||
code: 'RATE_LIMIT_EXCEEDED'
|
||||
},
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
// 인증된 사용자는 더 많은 요청 허용
|
||||
skip: (req) => {
|
||||
// Authorization 헤더가 있으면 Rate Limit 완화
|
||||
return req.headers.authorization && req.headers.authorization.startsWith('Bearer ');
|
||||
}
|
||||
legacyHeaders: false
|
||||
});
|
||||
|
||||
// 로그인 시도 제한 (브루트포스 방지)
|
||||
|
||||
@@ -16,13 +16,11 @@ const notificationRecipientController = {
|
||||
// 전체 수신자 목록 (유형별 그룹화)
|
||||
getAll: async (req, res) => {
|
||||
try {
|
||||
console.log('🔔 알림 수신자 목록 조회 시작');
|
||||
const recipients = await notificationRecipientModel.getAll();
|
||||
console.log('✅ 알림 수신자 목록 조회 완료:', recipients);
|
||||
res.json({ success: true, data: recipients });
|
||||
} catch (error) {
|
||||
console.error('❌ 수신자 목록 조회 오류:', error.message);
|
||||
console.error('❌ 스택:', error.stack);
|
||||
console.error(' 수신자 목록 조회 오류:', error.message);
|
||||
console.error(' 스택:', error.stack);
|
||||
res.status(500).json({ success: false, error: '수신자 목록 조회 실패', detail: error.message });
|
||||
}
|
||||
},
|
||||
|
||||
@@ -682,8 +682,7 @@ const resetUserPassword = asyncHandler(async (req, res) => {
|
||||
throw new NotFoundError('사용자를 찾을 수 없습니다');
|
||||
}
|
||||
|
||||
// 비밀번호를 000000으로 초기화
|
||||
const hashedPassword = await bcrypt.hash('000000', 10);
|
||||
const hashedPassword = await bcrypt.hash(process.env.DEFAULT_PASSWORD || 'changeme!1', 10);
|
||||
await db.execute(
|
||||
'UPDATE users SET password = ?, password_changed_at = NULL, updated_at = NOW() WHERE user_id = ?',
|
||||
[hashedPassword, id]
|
||||
|
||||
@@ -21,37 +21,32 @@ const getAnalysisFilters = asyncHandler(async (req, res) => {
|
||||
const db = await getDb();
|
||||
|
||||
try {
|
||||
// 프로젝트 목록
|
||||
const [projects] = await db.query(`
|
||||
SELECT DISTINCT p.project_id, p.project_name
|
||||
FROM projects p
|
||||
INNER JOIN daily_work_reports dwr ON p.project_id = dwr.project_id
|
||||
ORDER BY p.project_name
|
||||
`);
|
||||
|
||||
// 작업자 목록
|
||||
const [workers] = await db.query(`
|
||||
SELECT DISTINCT w.user_id, w.worker_name
|
||||
FROM workers w
|
||||
INNER JOIN daily_work_reports dwr ON w.user_id = dwr.user_id
|
||||
ORDER BY w.worker_name
|
||||
`);
|
||||
|
||||
// 작업 유형 목록
|
||||
const [workTypes] = await db.query(`
|
||||
SELECT DISTINCT wt.id as work_type_id, wt.name as work_type_name
|
||||
FROM work_types wt
|
||||
INNER JOIN daily_work_reports dwr ON wt.id = dwr.work_type_id
|
||||
ORDER BY wt.name
|
||||
`);
|
||||
|
||||
// 날짜 범위
|
||||
const [dateRange] = await db.query(`
|
||||
SELECT
|
||||
MIN(report_date) as min_date,
|
||||
MAX(report_date) as max_date
|
||||
FROM daily_work_reports
|
||||
`);
|
||||
const [[projects], [workers], [workTypes], [dateRange]] = await Promise.all([
|
||||
db.query(`
|
||||
SELECT DISTINCT p.project_id, p.project_name
|
||||
FROM projects p
|
||||
INNER JOIN daily_work_reports dwr ON p.project_id = dwr.project_id
|
||||
ORDER BY p.project_name
|
||||
`),
|
||||
db.query(`
|
||||
SELECT DISTINCT w.user_id, w.worker_name
|
||||
FROM workers w
|
||||
INNER JOIN daily_work_reports dwr ON w.user_id = dwr.user_id
|
||||
ORDER BY w.worker_name
|
||||
`),
|
||||
db.query(`
|
||||
SELECT DISTINCT wt.id as work_type_id, wt.name as work_type_name
|
||||
FROM work_types wt
|
||||
INNER JOIN daily_work_reports dwr ON wt.id = dwr.work_type_id
|
||||
ORDER BY wt.name
|
||||
`),
|
||||
db.query(`
|
||||
SELECT
|
||||
MIN(report_date) as min_date,
|
||||
MAX(report_date) as max_date
|
||||
FROM daily_work_reports
|
||||
`),
|
||||
]);
|
||||
|
||||
logger.info('분석 필터 데이터 조회 성공', {
|
||||
projects: projects.length,
|
||||
@@ -131,115 +126,108 @@ const getAnalyticsByPeriod = asyncHandler(async (req, res) => {
|
||||
WHERE ${whereClause}
|
||||
`;
|
||||
|
||||
const [overallStats] = await db.query(overallSql, queryParams);
|
||||
|
||||
// 2. 일별 통계
|
||||
const dailyStatsSql = `
|
||||
SELECT
|
||||
dwr.report_date,
|
||||
SUM(dwr.work_hours) as daily_hours,
|
||||
COUNT(*) as daily_entries,
|
||||
COUNT(DISTINCT dwr.user_id) as daily_workers
|
||||
FROM daily_work_reports dwr
|
||||
WHERE ${whereClause}
|
||||
GROUP BY dwr.report_date
|
||||
ORDER BY dwr.report_date ASC
|
||||
`;
|
||||
|
||||
const [dailyStats] = await db.query(dailyStatsSql, queryParams);
|
||||
|
||||
// 3. 일별 에러 통계
|
||||
const dailyErrorStatsSql = `
|
||||
SELECT
|
||||
dwr.report_date,
|
||||
COUNT(CASE WHEN dwr.error_type_id IS NOT NULL THEN 1 END) as daily_errors,
|
||||
COUNT(*) as daily_total,
|
||||
ROUND((COUNT(CASE WHEN dwr.error_type_id IS NOT NULL THEN 1 END) / COUNT(*)) * 100, 2) as daily_error_rate
|
||||
FROM daily_work_reports dwr
|
||||
WHERE ${whereClause}
|
||||
GROUP BY dwr.report_date
|
||||
ORDER BY dwr.report_date ASC
|
||||
`;
|
||||
|
||||
const [dailyErrorStats] = await db.query(dailyErrorStatsSql, queryParams);
|
||||
|
||||
// 4. 에러 유형별 분석
|
||||
const errorAnalysisSql = `
|
||||
SELECT
|
||||
et.id as error_type_id,
|
||||
et.name as error_type_name,
|
||||
COUNT(*) as error_count,
|
||||
SUM(dwr.work_hours) as error_hours,
|
||||
ROUND((COUNT(*) / (SELECT COUNT(*) FROM daily_work_reports WHERE error_type_id IS NOT NULL)) * 100, 2) as error_percentage
|
||||
FROM daily_work_reports dwr
|
||||
LEFT JOIN error_types et ON dwr.error_type_id = et.id
|
||||
WHERE ${whereClause} AND dwr.error_type_id IS NOT NULL
|
||||
GROUP BY et.id, et.name
|
||||
ORDER BY error_count DESC
|
||||
`;
|
||||
|
||||
const [errorAnalysis] = await db.query(errorAnalysisSql, queryParams);
|
||||
|
||||
// 5. 작업 유형별 분석
|
||||
const workTypeAnalysisSql = `
|
||||
SELECT
|
||||
wt.id as work_type_id,
|
||||
wt.name as work_type_name,
|
||||
COUNT(*) as work_count,
|
||||
SUM(dwr.work_hours) as total_hours,
|
||||
AVG(dwr.work_hours) as avg_hours,
|
||||
COUNT(CASE WHEN dwr.error_type_id IS NOT NULL THEN 1 END) as error_count,
|
||||
ROUND((COUNT(CASE WHEN dwr.error_type_id IS NOT NULL THEN 1 END) / COUNT(*)) * 100, 2) as error_rate
|
||||
FROM daily_work_reports dwr
|
||||
LEFT JOIN work_types wt ON dwr.work_type_id = wt.id
|
||||
WHERE ${whereClause}
|
||||
GROUP BY wt.id, wt.name
|
||||
ORDER BY total_hours DESC
|
||||
`;
|
||||
|
||||
const [workTypeAnalysis] = await db.query(workTypeAnalysisSql, queryParams);
|
||||
|
||||
// 6. 작업자별 성과 분석
|
||||
const workerAnalysisSql = `
|
||||
SELECT
|
||||
w.user_id,
|
||||
w.worker_name,
|
||||
COUNT(*) as total_entries,
|
||||
SUM(dwr.work_hours) as total_hours,
|
||||
AVG(dwr.work_hours) as avg_hours_per_entry,
|
||||
COUNT(DISTINCT dwr.project_id) as projects_worked,
|
||||
COUNT(DISTINCT dwr.report_date) as working_days,
|
||||
COUNT(CASE WHEN dwr.error_type_id IS NOT NULL THEN 1 END) as error_count,
|
||||
ROUND((COUNT(CASE WHEN dwr.error_type_id IS NOT NULL THEN 1 END) / COUNT(*)) * 100, 2) as error_rate
|
||||
FROM daily_work_reports dwr
|
||||
LEFT JOIN workers w ON dwr.user_id = w.user_id
|
||||
WHERE ${whereClause}
|
||||
GROUP BY w.user_id, w.worker_name
|
||||
ORDER BY total_hours DESC
|
||||
`;
|
||||
|
||||
const [workerAnalysis] = await db.query(workerAnalysisSql, queryParams);
|
||||
|
||||
// 7. 프로젝트별 분석
|
||||
const projectAnalysisSql = `
|
||||
SELECT
|
||||
p.project_id,
|
||||
p.project_name,
|
||||
COUNT(*) as total_entries,
|
||||
SUM(dwr.work_hours) as total_hours,
|
||||
COUNT(DISTINCT dwr.user_id) as workers_count,
|
||||
COUNT(DISTINCT dwr.report_date) as working_days,
|
||||
AVG(dwr.work_hours) as avg_hours_per_entry,
|
||||
COUNT(CASE WHEN dwr.error_type_id IS NOT NULL THEN 1 END) as error_count,
|
||||
ROUND((COUNT(CASE WHEN dwr.error_type_id IS NOT NULL THEN 1 END) / COUNT(*)) * 100, 2) as error_rate
|
||||
FROM daily_work_reports dwr
|
||||
LEFT JOIN projects p ON dwr.project_id = p.project_id
|
||||
WHERE ${whereClause}
|
||||
GROUP BY p.project_id, p.project_name
|
||||
ORDER BY total_hours DESC
|
||||
`;
|
||||
|
||||
const [projectAnalysis] = await db.query(projectAnalysisSql, queryParams);
|
||||
const [
|
||||
[overallStats],
|
||||
[dailyStats],
|
||||
[dailyErrorStats],
|
||||
[errorAnalysis],
|
||||
[workTypeAnalysis],
|
||||
[workerAnalysis],
|
||||
[projectAnalysis],
|
||||
] = await Promise.all([
|
||||
// 1. 전체 요약 통계
|
||||
db.query(overallSql, queryParams),
|
||||
// 2. 일별 통계
|
||||
db.query(`
|
||||
SELECT
|
||||
dwr.report_date,
|
||||
SUM(dwr.work_hours) as daily_hours,
|
||||
COUNT(*) as daily_entries,
|
||||
COUNT(DISTINCT dwr.user_id) as daily_workers
|
||||
FROM daily_work_reports dwr
|
||||
WHERE ${whereClause}
|
||||
GROUP BY dwr.report_date
|
||||
ORDER BY dwr.report_date ASC
|
||||
`, queryParams),
|
||||
// 3. 일별 에러 통계
|
||||
db.query(`
|
||||
SELECT
|
||||
dwr.report_date,
|
||||
COUNT(CASE WHEN dwr.error_type_id IS NOT NULL THEN 1 END) as daily_errors,
|
||||
COUNT(*) as daily_total,
|
||||
ROUND((COUNT(CASE WHEN dwr.error_type_id IS NOT NULL THEN 1 END) / COUNT(*)) * 100, 2) as daily_error_rate
|
||||
FROM daily_work_reports dwr
|
||||
WHERE ${whereClause}
|
||||
GROUP BY dwr.report_date
|
||||
ORDER BY dwr.report_date ASC
|
||||
`, queryParams),
|
||||
// 4. 에러 유형별 분석
|
||||
db.query(`
|
||||
SELECT
|
||||
et.id as error_type_id,
|
||||
et.name as error_type_name,
|
||||
COUNT(*) as error_count,
|
||||
SUM(dwr.work_hours) as error_hours,
|
||||
ROUND((COUNT(*) / (SELECT COUNT(*) FROM daily_work_reports WHERE error_type_id IS NOT NULL)) * 100, 2) as error_percentage
|
||||
FROM daily_work_reports dwr
|
||||
LEFT JOIN error_types et ON dwr.error_type_id = et.id
|
||||
WHERE ${whereClause} AND dwr.error_type_id IS NOT NULL
|
||||
GROUP BY et.id, et.name
|
||||
ORDER BY error_count DESC
|
||||
`, queryParams),
|
||||
// 5. 작업 유형별 분석
|
||||
db.query(`
|
||||
SELECT
|
||||
wt.id as work_type_id,
|
||||
wt.name as work_type_name,
|
||||
COUNT(*) as work_count,
|
||||
SUM(dwr.work_hours) as total_hours,
|
||||
AVG(dwr.work_hours) as avg_hours,
|
||||
COUNT(CASE WHEN dwr.error_type_id IS NOT NULL THEN 1 END) as error_count,
|
||||
ROUND((COUNT(CASE WHEN dwr.error_type_id IS NOT NULL THEN 1 END) / COUNT(*)) * 100, 2) as error_rate
|
||||
FROM daily_work_reports dwr
|
||||
LEFT JOIN work_types wt ON dwr.work_type_id = wt.id
|
||||
WHERE ${whereClause}
|
||||
GROUP BY wt.id, wt.name
|
||||
ORDER BY total_hours DESC
|
||||
`, queryParams),
|
||||
// 6. 작업자별 성과 분석
|
||||
db.query(`
|
||||
SELECT
|
||||
w.user_id,
|
||||
w.worker_name,
|
||||
COUNT(*) as total_entries,
|
||||
SUM(dwr.work_hours) as total_hours,
|
||||
AVG(dwr.work_hours) as avg_hours_per_entry,
|
||||
COUNT(DISTINCT dwr.project_id) as projects_worked,
|
||||
COUNT(DISTINCT dwr.report_date) as working_days,
|
||||
COUNT(CASE WHEN dwr.error_type_id IS NOT NULL THEN 1 END) as error_count,
|
||||
ROUND((COUNT(CASE WHEN dwr.error_type_id IS NOT NULL THEN 1 END) / COUNT(*)) * 100, 2) as error_rate
|
||||
FROM daily_work_reports dwr
|
||||
LEFT JOIN workers w ON dwr.user_id = w.user_id
|
||||
WHERE ${whereClause}
|
||||
GROUP BY w.user_id, w.worker_name
|
||||
ORDER BY total_hours DESC
|
||||
`, queryParams),
|
||||
// 7. 프로젝트별 분석
|
||||
db.query(`
|
||||
SELECT
|
||||
p.project_id,
|
||||
p.project_name,
|
||||
COUNT(*) as total_entries,
|
||||
SUM(dwr.work_hours) as total_hours,
|
||||
COUNT(DISTINCT dwr.user_id) as workers_count,
|
||||
COUNT(DISTINCT dwr.report_date) as working_days,
|
||||
AVG(dwr.work_hours) as avg_hours_per_entry,
|
||||
COUNT(CASE WHEN dwr.error_type_id IS NOT NULL THEN 1 END) as error_count,
|
||||
ROUND((COUNT(CASE WHEN dwr.error_type_id IS NOT NULL THEN 1 END) / COUNT(*)) * 100, 2) as error_rate
|
||||
FROM daily_work_reports dwr
|
||||
LEFT JOIN projects p ON dwr.project_id = p.project_id
|
||||
WHERE ${whereClause}
|
||||
GROUP BY p.project_id, p.project_name
|
||||
ORDER BY total_hours DESC
|
||||
`, queryParams),
|
||||
]);
|
||||
|
||||
logger.info('기간별 분석 데이터 조회 성공', {
|
||||
start_date,
|
||||
|
||||
@@ -33,7 +33,7 @@ exports.createWorker = asyncHandler(async (req, res) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const username = await generateUniqueUsername(workerData.worker_name, db);
|
||||
const hashedPassword = await bcrypt.hash('1234', 10);
|
||||
const hashedPassword = await bcrypt.hash(process.env.DEFAULT_PASSWORD || 'changeme!1', 10);
|
||||
|
||||
// User 역할 조회
|
||||
const [userRole] = await db.query('SELECT id FROM roles WHERE name = ?', ['User']);
|
||||
@@ -139,13 +139,6 @@ exports.updateWorker = asyncHandler(async (req, res) => {
|
||||
const workerData = { ...req.body, user_id: id };
|
||||
const createAccount = req.body.create_account;
|
||||
|
||||
console.log('🔧 작업자 수정 요청:', {
|
||||
user_id: id,
|
||||
받은데이터: req.body,
|
||||
처리할데이터: workerData,
|
||||
create_account: createAccount
|
||||
});
|
||||
|
||||
// 먼저 현재 작업자 정보 조회 (계정 여부 확인용, user_id 기준)
|
||||
const currentWorker = await workerModel.getByUserId(id);
|
||||
|
||||
@@ -166,7 +159,7 @@ exports.updateWorker = asyncHandler(async (req, res) => {
|
||||
// 계정 생성
|
||||
try {
|
||||
const username = await generateUniqueUsername(workerData.worker_name, db);
|
||||
const hashedPassword = await bcrypt.hash('1234', 10);
|
||||
const hashedPassword = await bcrypt.hash(process.env.DEFAULT_PASSWORD || 'changeme!1', 10);
|
||||
|
||||
const [userRole] = await db.query('SELECT id FROM roles WHERE name = ?', ['User']);
|
||||
|
||||
|
||||
@@ -13,10 +13,8 @@ async function createAttendanceTables() {
|
||||
database: 'hyungi'
|
||||
});
|
||||
|
||||
console.log('✅ MySQL 연결 성공');
|
||||
|
||||
// 1. 근로 유형 테이블 생성
|
||||
console.log('📋 근로 유형 테이블 생성 중...');
|
||||
await connection.execute(`
|
||||
CREATE TABLE IF NOT EXISTS work_attendance_types (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
@@ -30,7 +28,6 @@ async function createAttendanceTables() {
|
||||
`);
|
||||
|
||||
// 2. 휴가 유형 테이블 생성
|
||||
console.log('🏖️ 휴가 유형 테이블 생성 중...');
|
||||
await connection.execute(`
|
||||
CREATE TABLE IF NOT EXISTS vacation_types (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
@@ -45,7 +42,6 @@ async function createAttendanceTables() {
|
||||
`);
|
||||
|
||||
// 3. 일일 근태 기록 테이블 생성
|
||||
console.log('📊 일일 근태 기록 테이블 생성 중...');
|
||||
await connection.execute(`
|
||||
CREATE TABLE IF NOT EXISTS daily_attendance_records (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
@@ -73,7 +69,6 @@ async function createAttendanceTables() {
|
||||
`);
|
||||
|
||||
// 4. 작업자 휴가 잔여 관리 테이블 생성
|
||||
console.log('📅 휴가 잔여 관리 테이블 생성 중...');
|
||||
await connection.execute(`
|
||||
CREATE TABLE IF NOT EXISTS worker_vacation_balance (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
@@ -92,7 +87,6 @@ async function createAttendanceTables() {
|
||||
`);
|
||||
|
||||
// 5. 기본 데이터 삽입
|
||||
console.log('📝 기본 데이터 삽입 중...');
|
||||
|
||||
// 근로 유형 기본 데이터
|
||||
await connection.execute(`
|
||||
@@ -116,7 +110,6 @@ async function createAttendanceTables() {
|
||||
`);
|
||||
|
||||
// 6. 휴가 전용 작업 유형 추가
|
||||
console.log('🏖️ 휴가 전용 작업 유형 추가 중...');
|
||||
await connection.execute(`
|
||||
INSERT IGNORE INTO work_types (name, description, is_active) VALUES
|
||||
('휴가', '연차, 반차, 병가 등 휴가 처리용', TRUE)
|
||||
@@ -128,40 +121,31 @@ async function createAttendanceTables() {
|
||||
ALTER TABLE daily_work_reports
|
||||
ADD COLUMN attendance_record_id INT NULL COMMENT '근태 기록 ID' AFTER updated_by
|
||||
`);
|
||||
console.log('✅ daily_work_reports 테이블에 attendance_record_id 컬럼 추가됨');
|
||||
} catch (error) {
|
||||
if (error.code !== 'ER_DUP_FIELDNAME') {
|
||||
console.log('⚠️ attendance_record_id 컬럼 추가 실패:', error.message);
|
||||
} else {
|
||||
console.log('✅ attendance_record_id 컬럼이 이미 존재함');
|
||||
}
|
||||
}
|
||||
|
||||
// 8. 인덱스 추가
|
||||
try {
|
||||
await connection.execute(`CREATE INDEX idx_attendance_record ON daily_work_reports(attendance_record_id)`);
|
||||
console.log('✅ attendance_record_id 인덱스 추가됨');
|
||||
} catch (error) {
|
||||
console.log('⚠️ 인덱스 추가 실패 (이미 존재할 수 있음):', error.message);
|
||||
}
|
||||
|
||||
console.log('🎉 근태 관리 DB 설정 완료!');
|
||||
console.log('');
|
||||
console.log('📋 생성된 테이블:');
|
||||
console.log(' - work_attendance_types (근로 유형)');
|
||||
console.log(' - vacation_types (휴가 유형)');
|
||||
console.log(' - daily_attendance_records (일일 근태 기록)');
|
||||
console.log(' - worker_vacation_balance (휴가 잔여 관리)');
|
||||
console.log('');
|
||||
console.log('✅ 기본 데이터도 모두 삽입되었습니다.');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ DB 설정 중 오류 발생:', error);
|
||||
console.error(' DB 설정 중 오류 발생:', error);
|
||||
|
||||
// 다른 연결 정보로 시도
|
||||
if (error.code === 'ECONNREFUSED' || error.code === 'ER_ACCESS_DENIED_ERROR') {
|
||||
console.log('');
|
||||
console.log('💡 다른 DB 연결 정보를 시도해보세요:');
|
||||
console.log(' - host: localhost 또는 127.0.0.1');
|
||||
console.log(' - port: 3306 (기본값)');
|
||||
console.log(' - user: root 또는 다른 사용자');
|
||||
@@ -181,11 +165,10 @@ async function createAttendanceTables() {
|
||||
if (require.main === module) {
|
||||
createAttendanceTables()
|
||||
.then(() => {
|
||||
console.log('✅ 설정 완료');
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('❌ 설정 실패:', error);
|
||||
console.error(' 설정 실패:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ exports.up = async function(knex) {
|
||||
.comment('재직 상태 (employed: 재직, resigned: 퇴사)');
|
||||
});
|
||||
|
||||
console.log('✅ workers 테이블에 employment_status 컬럼 추가 완료');
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -23,5 +22,4 @@ exports.down = async function(knex) {
|
||||
table.dropColumn('employment_status');
|
||||
});
|
||||
|
||||
console.log('✅ workers 테이블에서 employment_status 컬럼 삭제 완료');
|
||||
};
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
*/
|
||||
|
||||
exports.up = async function(knex) {
|
||||
console.log('⏳ Workers 테이블에 salary, base_annual_leave 컬럼 추가 중...');
|
||||
|
||||
await knex.schema.alterTable('workers', (table) => {
|
||||
// 급여 정보 (선택 사항, NULL 허용)
|
||||
@@ -18,16 +17,13 @@ exports.up = async function(knex) {
|
||||
table.integer('base_annual_leave').defaultTo(15).notNullable().comment('기본 연차 일수');
|
||||
});
|
||||
|
||||
console.log('✅ Workers 테이블 컬럼 추가 완료');
|
||||
};
|
||||
|
||||
exports.down = async function(knex) {
|
||||
console.log('⏳ Workers 테이블에서 salary, base_annual_leave 컬럼 제거 중...');
|
||||
|
||||
await knex.schema.alterTable('workers', (table) => {
|
||||
table.dropColumn('salary');
|
||||
table.dropColumn('base_annual_leave');
|
||||
});
|
||||
|
||||
console.log('✅ Workers 테이블 컬럼 제거 완료');
|
||||
};
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
*/
|
||||
|
||||
exports.up = async function(knex) {
|
||||
console.log('⏳ 출근/근태 관련 테이블 생성 중...');
|
||||
|
||||
// 1. 출근 유형 테이블
|
||||
await knex.schema.createTable('work_attendance_types', (table) => {
|
||||
@@ -22,7 +21,6 @@ exports.up = async function(knex) {
|
||||
table.timestamp('created_at').defaultTo(knex.fn.now());
|
||||
table.timestamp('updated_at').defaultTo(knex.fn.now());
|
||||
});
|
||||
console.log('✅ work_attendance_types 테이블 생성 완료');
|
||||
|
||||
// 초기 데이터 입력
|
||||
await knex('work_attendance_types').insert([
|
||||
@@ -32,7 +30,6 @@ exports.up = async function(knex) {
|
||||
{ type_code: 'ABSENT', type_name: '결근', description: '무단 결근' },
|
||||
{ type_code: 'VACATION', type_name: '휴가', description: '승인된 휴가' }
|
||||
]);
|
||||
console.log('✅ work_attendance_types 초기 데이터 입력 완료');
|
||||
|
||||
// 2. 휴가 유형 테이블
|
||||
await knex.schema.createTable('vacation_types', (table) => {
|
||||
@@ -44,7 +41,6 @@ exports.up = async function(knex) {
|
||||
table.timestamp('created_at').defaultTo(knex.fn.now());
|
||||
table.timestamp('updated_at').defaultTo(knex.fn.now());
|
||||
});
|
||||
console.log('✅ vacation_types 테이블 생성 완료');
|
||||
|
||||
// 초기 데이터 입력
|
||||
await knex('vacation_types').insert([
|
||||
@@ -53,7 +49,6 @@ exports.up = async function(knex) {
|
||||
{ type_code: 'SICK', type_name: '병가', deduct_days: 1.0 },
|
||||
{ type_code: 'SPECIAL', type_name: '경조사', deduct_days: 0 }
|
||||
]);
|
||||
console.log('✅ vacation_types 초기 데이터 입력 완료');
|
||||
|
||||
// 3. 일일 출근 기록 테이블
|
||||
await knex.schema.createTable('daily_attendance_records', (table) => {
|
||||
@@ -78,7 +73,6 @@ exports.up = async function(knex) {
|
||||
table.foreign('vacation_type_id').references('vacation_types.id');
|
||||
table.foreign('created_by').references('users.user_id');
|
||||
});
|
||||
console.log('✅ daily_attendance_records 테이블 생성 완료');
|
||||
|
||||
// 4. 작업자 연차 잔액 테이블
|
||||
await knex.schema.createTable('worker_vacation_balance', (table) => {
|
||||
@@ -95,18 +89,14 @@ exports.up = async function(knex) {
|
||||
table.unique(['worker_id', 'year']);
|
||||
table.foreign('worker_id').references('workers.worker_id').onDelete('CASCADE');
|
||||
});
|
||||
console.log('✅ worker_vacation_balance 테이블 생성 완료');
|
||||
|
||||
console.log('✅ 모든 출근/근태 관련 테이블 생성 완료');
|
||||
};
|
||||
|
||||
exports.down = async function(knex) {
|
||||
console.log('⏳ 출근/근태 관련 테이블 제거 중...');
|
||||
|
||||
await knex.schema.dropTableIfExists('worker_vacation_balance');
|
||||
await knex.schema.dropTableIfExists('daily_attendance_records');
|
||||
await knex.schema.dropTableIfExists('vacation_types');
|
||||
await knex.schema.dropTableIfExists('work_attendance_types');
|
||||
|
||||
console.log('✅ 모든 출근/근태 관련 테이블 제거 완료');
|
||||
};
|
||||
|
||||
@@ -14,7 +14,6 @@ const bcrypt = require('bcrypt');
|
||||
const { generateUniqueUsername } = require('../../utils/hangulToRoman');
|
||||
|
||||
exports.up = async function(knex) {
|
||||
console.log('⏳ 기존 작업자들에게 계정 자동 생성 중...');
|
||||
|
||||
// 1. 계정이 없는 작업자 조회
|
||||
const workersWithoutAccount = await knex('workers')
|
||||
@@ -28,10 +27,8 @@ exports.up = async function(knex) {
|
||||
'workers.annual_leave'
|
||||
);
|
||||
|
||||
console.log(`📊 계정이 없는 작업자: ${workersWithoutAccount.length}명`);
|
||||
|
||||
if (workersWithoutAccount.length === 0) {
|
||||
console.log('ℹ️ 계정이 필요한 작업자가 없습니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -69,7 +66,6 @@ exports.up = async function(knex) {
|
||||
updated_at: knex.fn.now()
|
||||
});
|
||||
|
||||
console.log(`✅ ${worker.worker_name} (ID: ${worker.worker_id}) → username: ${username}`);
|
||||
successCount++;
|
||||
|
||||
// 현재 연도 연차 잔액 초기화
|
||||
@@ -84,21 +80,15 @@ exports.up = async function(knex) {
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error(`❌ ${worker.worker_name} 계정 생성 실패:`, error.message);
|
||||
console.error(` ${worker.worker_name} 계정 생성 실패:`, error.message);
|
||||
errorCount++;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\n📊 작업 완료: 성공 ${successCount}명, 실패 ${errorCount}명`);
|
||||
console.log(`🔐 초기 비밀번호: ${initialPassword} (모든 계정 공통)`);
|
||||
console.log('⚠️ 사용자들에게 첫 로그인 후 비밀번호를 변경하도록 안내해주세요!');
|
||||
};
|
||||
|
||||
exports.down = async function(knex) {
|
||||
console.log('⏳ 자동 생성된 계정 제거 중...');
|
||||
|
||||
// 이 마이그레이션으로 생성된 계정은 구분하기 어려우므로
|
||||
// rollback 시 주의가 필요합니다.
|
||||
console.log('⚠️ 경고: 이 마이그레이션의 rollback은 권장하지 않습니다.');
|
||||
console.log('ℹ️ 필요시 수동으로 users 테이블을 관리하세요.');
|
||||
};
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
*/
|
||||
|
||||
exports.up = async function(knex) {
|
||||
console.log('⏳ 게스트 역할 추가 중...');
|
||||
|
||||
// 1. Guest 역할 추가
|
||||
const [guestRoleId] = await knex('roles').insert({
|
||||
@@ -17,7 +16,6 @@ exports.up = async function(knex) {
|
||||
created_at: knex.fn.now(),
|
||||
updated_at: knex.fn.now()
|
||||
});
|
||||
console.log(`✅ Guest 역할 추가 완료 (ID: ${guestRoleId})`);
|
||||
|
||||
// 2. 게스트 전용 페이지 추가
|
||||
await knex('pages').insert({
|
||||
@@ -29,13 +27,10 @@ exports.up = async function(knex) {
|
||||
created_at: knex.fn.now(),
|
||||
updated_at: knex.fn.now()
|
||||
});
|
||||
console.log('✅ 게스트 전용 페이지 추가 완료 (신고 채널)');
|
||||
|
||||
console.log('✅ 게스트 역할 추가 완료');
|
||||
};
|
||||
|
||||
exports.down = async function(knex) {
|
||||
console.log('⏳ 게스트 역할 제거 중...');
|
||||
|
||||
// 페이지 제거
|
||||
await knex('pages')
|
||||
@@ -47,5 +42,4 @@ exports.down = async function(knex) {
|
||||
.where('name', 'Guest')
|
||||
.delete();
|
||||
|
||||
console.log('✅ 게스트 역할 제거 완료');
|
||||
};
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
*/
|
||||
|
||||
exports.up = async function(knex) {
|
||||
console.log('⏳ TBM 시스템 테이블 생성 중...');
|
||||
|
||||
// 1. TBM 세션 테이블 (아침 미팅)
|
||||
await knex.schema.createTable('tbm_sessions', (table) => {
|
||||
@@ -35,7 +34,6 @@ exports.up = async function(knex) {
|
||||
table.foreign('project_id').references('projects.project_id').onDelete('SET NULL');
|
||||
table.foreign('created_by').references('users.user_id');
|
||||
});
|
||||
console.log('✅ tbm_sessions 테이블 생성 완료');
|
||||
|
||||
// 2. TBM 팀 구성 테이블 (리더가 선택한 팀원들)
|
||||
await knex.schema.createTable('tbm_team_assignments', (table) => {
|
||||
@@ -53,7 +51,6 @@ exports.up = async function(knex) {
|
||||
table.foreign('session_id').references('tbm_sessions.session_id').onDelete('CASCADE');
|
||||
table.foreign('worker_id').references('workers.worker_id');
|
||||
});
|
||||
console.log('✅ tbm_team_assignments 테이블 생성 완료');
|
||||
|
||||
// 3. TBM 안전 체크리스트 마스터 테이블
|
||||
await knex.schema.createTable('tbm_safety_checks', (table) => {
|
||||
@@ -69,7 +66,6 @@ exports.up = async function(knex) {
|
||||
|
||||
table.index('check_category');
|
||||
});
|
||||
console.log('✅ tbm_safety_checks 테이블 생성 완료');
|
||||
|
||||
// 초기 안전 체크리스트 데이터
|
||||
await knex('tbm_safety_checks').insert([
|
||||
@@ -97,7 +93,6 @@ exports.up = async function(knex) {
|
||||
{ check_category: 'EMERGENCY', check_item: '소화기 위치 확인', display_order: 31, is_required: true },
|
||||
{ check_category: 'EMERGENCY', check_item: '응급처치 키트 위치 확인', display_order: 32, is_required: true },
|
||||
]);
|
||||
console.log('✅ tbm_safety_checks 초기 데이터 입력 완료');
|
||||
|
||||
// 4. TBM 안전 체크 기록 테이블
|
||||
await knex.schema.createTable('tbm_safety_records', (table) => {
|
||||
@@ -115,7 +110,6 @@ exports.up = async function(knex) {
|
||||
table.foreign('check_id').references('tbm_safety_checks.check_id');
|
||||
table.foreign('checked_by').references('users.user_id');
|
||||
});
|
||||
console.log('✅ tbm_safety_records 테이블 생성 완료');
|
||||
|
||||
// 5. 작업 인계 테이블 (반차/조퇴 시)
|
||||
await knex.schema.createTable('team_handovers', (table) => {
|
||||
@@ -140,13 +134,10 @@ exports.up = async function(knex) {
|
||||
table.foreign('to_leader_id').references('workers.worker_id');
|
||||
table.foreign('confirmed_by').references('users.user_id');
|
||||
});
|
||||
console.log('✅ team_handovers 테이블 생성 완료');
|
||||
|
||||
console.log('✅ 모든 TBM 시스템 테이블 생성 완료');
|
||||
};
|
||||
|
||||
exports.down = async function(knex) {
|
||||
console.log('⏳ TBM 시스템 테이블 제거 중...');
|
||||
|
||||
await knex.schema.dropTableIfExists('team_handovers');
|
||||
await knex.schema.dropTableIfExists('tbm_safety_records');
|
||||
@@ -154,5 +145,4 @@ exports.down = async function(knex) {
|
||||
await knex.schema.dropTableIfExists('tbm_team_assignments');
|
||||
await knex.schema.dropTableIfExists('tbm_sessions');
|
||||
|
||||
console.log('✅ 모든 TBM 시스템 테이블 제거 완료');
|
||||
};
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
*/
|
||||
|
||||
exports.up = async function(knex) {
|
||||
console.log('⏳ TBM 페이지 등록 중...');
|
||||
|
||||
// TBM 페이지 추가
|
||||
await knex('pages').insert([
|
||||
@@ -21,13 +20,10 @@ exports.up = async function(knex) {
|
||||
}
|
||||
]);
|
||||
|
||||
console.log('✅ TBM 페이지 등록 완료');
|
||||
};
|
||||
|
||||
exports.down = async function(knex) {
|
||||
console.log('⏳ TBM 페이지 제거 중...');
|
||||
|
||||
await knex('pages').where('page_key', 'tbm').del();
|
||||
|
||||
console.log('✅ TBM 페이지 제거 완료');
|
||||
};
|
||||
|
||||
@@ -24,7 +24,6 @@ exports.up = function(knex) {
|
||||
table.index('work_type_id');
|
||||
table.index('is_active');
|
||||
}).then(() => {
|
||||
console.log('✅ tasks 테이블 생성 완료');
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -25,7 +25,6 @@ exports.up = function(knex) {
|
||||
table.index('work_type_id');
|
||||
table.index('task_id');
|
||||
}).then(() => {
|
||||
console.log('✅ tbm_sessions 테이블에 work_type_id, task_id 컬럼 추가 완료');
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -143,10 +143,8 @@ exports.up = async function(knex) {
|
||||
}
|
||||
]);
|
||||
|
||||
console.log('✅ 현재 사용 중인 페이지 목록 업데이트 완료');
|
||||
};
|
||||
|
||||
exports.down = async function(knex) {
|
||||
await knex('pages').del();
|
||||
console.log('✅ 페이지 목록 삭제 완료');
|
||||
};
|
||||
|
||||
@@ -10,7 +10,6 @@ exports.up = async function(knex) {
|
||||
table.string('layout_image', 500).nullable().comment('작업장 레이아웃 이미지 경로');
|
||||
});
|
||||
|
||||
console.log('✅ workplaces 테이블에 layout_image 컬럼 추가 완료');
|
||||
};
|
||||
|
||||
exports.down = async function(knex) {
|
||||
@@ -18,5 +17,4 @@ exports.down = async function(knex) {
|
||||
table.dropColumn('layout_image');
|
||||
});
|
||||
|
||||
console.log('✅ workplaces 테이블에서 layout_image 컬럼 제거 완료');
|
||||
};
|
||||
|
||||
@@ -39,10 +39,8 @@ exports.up = async function(knex) {
|
||||
table.index('status');
|
||||
});
|
||||
|
||||
console.log('✅ equipments 테이블 생성 완료');
|
||||
};
|
||||
|
||||
exports.down = async function(knex) {
|
||||
await knex.schema.dropTableIfExists('equipments');
|
||||
console.log('✅ equipments 테이블 삭제 완료');
|
||||
};
|
||||
|
||||
@@ -48,10 +48,8 @@ exports.up = async function(knex) {
|
||||
table.index(['start_date', 'end_date'], 'idx_vacation_requests_dates');
|
||||
});
|
||||
|
||||
console.log('✅ vacation_requests 테이블 생성 완료');
|
||||
};
|
||||
|
||||
exports.down = async function(knex) {
|
||||
await knex.schema.dropTableIfExists('vacation_requests');
|
||||
console.log('✅ vacation_requests 테이블 삭제 완료');
|
||||
};
|
||||
|
||||
@@ -45,7 +45,6 @@ exports.up = async function(knex) {
|
||||
}
|
||||
]);
|
||||
|
||||
console.log('✅ 출퇴근 관리 페이지 4개 등록 완료');
|
||||
|
||||
// Admin 사용자(user_id=1)에게 페이지 접근 권한 부여
|
||||
const adminUserId = 1;
|
||||
@@ -66,7 +65,6 @@ exports.up = async function(knex) {
|
||||
}));
|
||||
|
||||
await knex('user_page_access').insert(accessRecords);
|
||||
console.log('✅ Admin 사용자에게 출퇴근 관리 페이지 접근 권한 부여 완료');
|
||||
};
|
||||
|
||||
exports.down = async function(knex) {
|
||||
@@ -80,5 +78,4 @@ exports.down = async function(knex) {
|
||||
])
|
||||
.delete();
|
||||
|
||||
console.log('✅ 출퇴근 관리 페이지 삭제 완료');
|
||||
};
|
||||
|
||||
@@ -18,9 +18,7 @@ exports.up = async function(knex) {
|
||||
.whereNotNull('id')
|
||||
.update({ is_present: true });
|
||||
|
||||
console.log('✅ is_present 컬럼 추가 완료');
|
||||
} else {
|
||||
console.log('⏭️ is_present 컬럼이 이미 존재합니다');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -33,7 +33,6 @@ exports.up = async function(knex) {
|
||||
}
|
||||
]);
|
||||
|
||||
console.log('✅ 휴가 관리 페이지 분리 완료 (기존 1개 → 신규 2개)');
|
||||
};
|
||||
|
||||
exports.down = async function(knex) {
|
||||
@@ -53,5 +52,4 @@ exports.down = async function(knex) {
|
||||
display_order: 50
|
||||
});
|
||||
|
||||
console.log('✅ 휴가 관리 페이지 롤백 완료');
|
||||
};
|
||||
|
||||
@@ -39,7 +39,6 @@ exports.up = async function(knex) {
|
||||
description: '경조사 휴가 (무급)'
|
||||
});
|
||||
|
||||
console.log('✅ vacation_types 테이블 확장 완료');
|
||||
};
|
||||
|
||||
exports.down = async function(knex) {
|
||||
@@ -51,5 +50,4 @@ exports.down = async function(knex) {
|
||||
table.dropColumn('is_system');
|
||||
});
|
||||
|
||||
console.log('✅ vacation_types 테이블 롤백 완료');
|
||||
};
|
||||
|
||||
@@ -37,7 +37,6 @@ exports.up = async function(knex) {
|
||||
COMMENT '잔여 일수'
|
||||
`);
|
||||
|
||||
console.log('✅ vacation_balance_details 테이블 생성 완료');
|
||||
|
||||
// 기존 worker_vacation_balance 데이터를 vacation_balance_details로 마이그레이션
|
||||
const existingBalances = await knex('worker_vacation_balance').select('*');
|
||||
@@ -75,7 +74,6 @@ exports.up = async function(knex) {
|
||||
|
||||
await knex('vacation_balance_details').insert(balanceDetails);
|
||||
|
||||
console.log(`✅ ${balanceDetails.length}건의 기존 휴가 데이터 마이그레이션 완료`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -83,5 +81,4 @@ exports.down = async function(knex) {
|
||||
// vacation_balance_details 테이블 삭제
|
||||
await knex.schema.dropTableIfExists('vacation_balance_details');
|
||||
|
||||
console.log('✅ vacation_balance_details 테이블 롤백 완료');
|
||||
};
|
||||
|
||||
@@ -26,7 +26,6 @@ exports.up = async function(knex) {
|
||||
}
|
||||
]);
|
||||
|
||||
console.log('✅ 휴가 관리 신규 페이지 2개 등록 완료');
|
||||
};
|
||||
|
||||
exports.down = async function(knex) {
|
||||
@@ -34,5 +33,4 @@ exports.down = async function(knex) {
|
||||
.whereIn('page_key', ['annual-vacation-overview', 'vacation-allocation'])
|
||||
.del();
|
||||
|
||||
console.log('✅ 휴가 관리 페이지 롤백 완료');
|
||||
};
|
||||
|
||||
@@ -140,7 +140,6 @@ exports.up = async function(knex) {
|
||||
table.index('request_id', 'idx_request_id');
|
||||
});
|
||||
|
||||
console.log('✅ 출입 신청 및 안전교육 시스템 테이블 생성 완료');
|
||||
};
|
||||
|
||||
exports.down = async function(knex) {
|
||||
@@ -149,5 +148,4 @@ exports.down = async function(knex) {
|
||||
await knex.schema.dropTableIfExists('workplace_visit_requests');
|
||||
await knex.schema.dropTableIfExists('visit_purpose_types');
|
||||
|
||||
console.log('✅ 출입 신청 및 안전교육 시스템 테이블 삭제 완료');
|
||||
};
|
||||
|
||||
@@ -36,7 +36,6 @@ exports.up = async function(knex) {
|
||||
display_order: 61
|
||||
});
|
||||
|
||||
console.log('✅ 출입 신청 및 안전관리 페이지 등록 완료');
|
||||
};
|
||||
|
||||
exports.down = async function(knex) {
|
||||
@@ -46,5 +45,4 @@ exports.down = async function(knex) {
|
||||
'safety-training-conduct'
|
||||
]).delete();
|
||||
|
||||
console.log('✅ 출입 신청 및 안전관리 페이지 삭제 완료');
|
||||
};
|
||||
|
||||
@@ -36,7 +36,6 @@ exports.up = async function(knex) {
|
||||
display_order: 18
|
||||
});
|
||||
|
||||
console.log('✅ 문제 신고 페이지 등록 완료');
|
||||
};
|
||||
|
||||
exports.down = async function(knex) {
|
||||
@@ -46,5 +45,4 @@ exports.down = async function(knex) {
|
||||
'issue-detail'
|
||||
]).delete();
|
||||
|
||||
console.log('✅ 문제 신고 페이지 삭제 완료');
|
||||
};
|
||||
|
||||
@@ -19,9 +19,7 @@ exports.up = async function(knex) {
|
||||
table.decimal('purchase_price', 15, 0).nullable().after('supplier').comment('구입가격');
|
||||
}
|
||||
});
|
||||
console.log('✅ equipments 테이블에 supplier, purchase_price 컬럼 추가 완료');
|
||||
} else {
|
||||
console.log('ℹ️ supplier, purchase_price 컬럼이 이미 존재합니다. 스킵합니다.');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -31,5 +29,4 @@ exports.down = async function(knex) {
|
||||
table.dropColumn('purchase_price');
|
||||
});
|
||||
|
||||
console.log('✅ equipments 테이블에서 supplier, purchase_price 컬럼 삭제 완료');
|
||||
};
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
*/
|
||||
|
||||
exports.up = async function(knex) {
|
||||
console.log('⏳ 일일순회점검 시스템 테이블 생성 중...');
|
||||
|
||||
// 1. 순회점검 체크리스트 마스터 테이블
|
||||
await knex.schema.createTable('patrol_checklist_items', (table) => {
|
||||
@@ -32,7 +31,6 @@ exports.up = async function(knex) {
|
||||
table.foreign('workplace_id').references('workplaces.workplace_id').onDelete('CASCADE');
|
||||
table.foreign('category_id').references('workplace_categories.category_id').onDelete('CASCADE');
|
||||
});
|
||||
console.log('✅ patrol_checklist_items 테이블 생성 완료');
|
||||
|
||||
// 초기 순회점검 체크리스트 데이터
|
||||
await knex('patrol_checklist_items').insert([
|
||||
@@ -58,7 +56,6 @@ exports.up = async function(knex) {
|
||||
{ check_category: 'ENVIRONMENT', check_item: '환기 상태', display_order: 31, is_required: true },
|
||||
{ check_category: 'ENVIRONMENT', check_item: '누수/누유 여부', display_order: 32, is_required: true },
|
||||
]);
|
||||
console.log('✅ patrol_checklist_items 초기 데이터 입력 완료');
|
||||
|
||||
// 2. 순회점검 세션 테이블
|
||||
await knex.schema.createTable('daily_patrol_sessions', (table) => {
|
||||
@@ -80,7 +77,6 @@ exports.up = async function(knex) {
|
||||
table.foreign('inspector_id').references('users.user_id');
|
||||
table.foreign('category_id').references('workplace_categories.category_id').onDelete('SET NULL');
|
||||
});
|
||||
console.log('✅ daily_patrol_sessions 테이블 생성 완료');
|
||||
|
||||
// 3. 순회점검 체크 기록 테이블
|
||||
await knex.schema.createTable('patrol_check_records', (table) => {
|
||||
@@ -100,7 +96,6 @@ exports.up = async function(knex) {
|
||||
table.foreign('workplace_id').references('workplaces.workplace_id').onDelete('CASCADE');
|
||||
table.foreign('check_item_id').references('patrol_checklist_items.item_id').onDelete('CASCADE');
|
||||
});
|
||||
console.log('✅ patrol_check_records 테이블 생성 완료');
|
||||
|
||||
// 4. 작업장 물품 현황 테이블
|
||||
await knex.schema.createTable('workplace_items', (table) => {
|
||||
@@ -129,7 +124,6 @@ exports.up = async function(knex) {
|
||||
table.foreign('created_by').references('users.user_id');
|
||||
table.foreign('updated_by').references('users.user_id');
|
||||
});
|
||||
console.log('✅ workplace_items 테이블 생성 완료');
|
||||
|
||||
// 물품 유형 코드 테이블 (선택적 확장용)
|
||||
await knex.schema.createTable('item_types', (table) => {
|
||||
@@ -148,13 +142,10 @@ exports.up = async function(knex) {
|
||||
{ type_code: 'tool', type_name: '공구/장비', icon: '🔧', color: '#8b5cf6', display_order: 4 },
|
||||
{ type_code: 'other', type_name: '기타', icon: '📍', color: '#6b7280', display_order: 5 },
|
||||
]);
|
||||
console.log('✅ item_types 테이블 생성 및 초기 데이터 완료');
|
||||
|
||||
console.log('✅ 모든 일일순회점검 시스템 테이블 생성 완료');
|
||||
};
|
||||
|
||||
exports.down = async function(knex) {
|
||||
console.log('⏳ 일일순회점검 시스템 테이블 제거 중...');
|
||||
|
||||
await knex.schema.dropTableIfExists('item_types');
|
||||
await knex.schema.dropTableIfExists('workplace_items');
|
||||
@@ -162,5 +153,4 @@ exports.down = async function(knex) {
|
||||
await knex.schema.dropTableIfExists('daily_patrol_sessions');
|
||||
await knex.schema.dropTableIfExists('patrol_checklist_items');
|
||||
|
||||
console.log('✅ 모든 일일순회점검 시스템 테이블 제거 완료');
|
||||
};
|
||||
|
||||
@@ -98,7 +98,6 @@ exports.up = async function(knex) {
|
||||
]);
|
||||
|
||||
if (unmapped.length > 0) {
|
||||
console.log('⚠️ 매핑되지 않은 error_type_id 발견:', unmapped);
|
||||
console.log(' 이 데이터는 수동으로 확인 필요');
|
||||
}
|
||||
|
||||
@@ -107,6 +106,5 @@ exports.up = async function(knex) {
|
||||
|
||||
exports.down = async function(knex) {
|
||||
// 롤백은 복잡하므로 로그만 출력
|
||||
console.log('⚠️ 이 마이그레이션은 자동 롤백을 지원하지 않습니다.');
|
||||
console.log(' 데이터 복구가 필요한 경우 백업에서 복원해주세요.');
|
||||
};
|
||||
|
||||
@@ -22,9 +22,7 @@ exports.up = async function(knex) {
|
||||
WHERE u.worker_id IS NOT NULL AND w.department_id IS NOT NULL
|
||||
`);
|
||||
|
||||
console.log('✅ users.department_id 컬럼 추가 및 기존 데이터 backfill 완료');
|
||||
} else {
|
||||
console.log('⏭️ users.department_id 컬럼이 이미 존재합니다');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -23,7 +23,6 @@ exports.up = async function(knex) {
|
||||
table.boolean('is_production').defaultTo(false).comment('생산직 부서 여부');
|
||||
});
|
||||
await knex.raw(`UPDATE departments SET is_production = TRUE WHERE department_name LIKE '%생산%'`);
|
||||
console.log('✅ departments.is_production 추가 완료');
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
@@ -41,7 +40,6 @@ exports.up = async function(knex) {
|
||||
SET s.department_id = d.department_id
|
||||
WHERE s.department IS NOT NULL
|
||||
`);
|
||||
console.log('✅ sso_users.department_id 추가 및 백필 완료');
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
@@ -62,7 +60,6 @@ exports.up = async function(knex) {
|
||||
`);
|
||||
// user_id에 인덱스 추가
|
||||
await knex.raw(`ALTER TABLE workers ADD INDEX idx_workers_user_id (user_id)`);
|
||||
console.log('✅ workers.user_id 추가 및 백필 완료');
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
@@ -85,7 +82,6 @@ exports.up = async function(knex) {
|
||||
for (const tableName of tablesWithWorkerId) {
|
||||
const tableExists = await knex.schema.hasTable(tableName);
|
||||
if (!tableExists) {
|
||||
console.log(`⏭️ ${tableName} 테이블이 존재하지 않음, 건너뜀`);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -103,7 +99,6 @@ exports.up = async function(knex) {
|
||||
`);
|
||||
// 인덱스 추가
|
||||
await knex.raw(`ALTER TABLE ${tableName} ADD INDEX idx_${tableName}_user_id (user_id)`);
|
||||
console.log(`✅ ${tableName}.user_id 추가 및 백필 완료`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,7 +116,6 @@ exports.up = async function(knex) {
|
||||
SET t.user_id = w.user_id
|
||||
WHERE w.user_id IS NOT NULL
|
||||
`);
|
||||
console.log('✅ DailyIssueReports.user_id 추가 및 백필 완료');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,7 +133,6 @@ exports.up = async function(knex) {
|
||||
SET t.user_id = w.user_id
|
||||
WHERE w.user_id IS NOT NULL
|
||||
`);
|
||||
console.log('✅ WorkReports.user_id 추가 및 백필 완료');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,7 +149,6 @@ exports.up = async function(knex) {
|
||||
WHERE w.user_id IS NOT NULL
|
||||
`);
|
||||
await knex.raw(`ALTER TABLE tbm_sessions ADD INDEX idx_tbm_sessions_leader_user_id (leader_user_id)`);
|
||||
console.log('✅ tbm_sessions.leader_user_id 추가 및 백필 완료');
|
||||
}
|
||||
|
||||
// team_handovers: from/to_leader_id → from/to_leader_user_id 추가
|
||||
@@ -178,10 +170,8 @@ exports.up = async function(knex) {
|
||||
SET t.to_leader_user_id = w2.user_id
|
||||
WHERE w2.user_id IS NOT NULL
|
||||
`);
|
||||
console.log('✅ team_handovers.from/to_leader_user_id 추가 및 백필 완료');
|
||||
}
|
||||
|
||||
console.log('🎉 Phase 1 마이그레이션 완료: 모든 테이블에 user_id 컬럼 추가 및 백필 완료');
|
||||
};
|
||||
|
||||
exports.down = async function(knex) {
|
||||
@@ -211,7 +201,6 @@ exports.down = async function(knex) {
|
||||
await knex.schema.table(tableName, (table) => {
|
||||
table.dropColumn(columnName);
|
||||
});
|
||||
console.log(`↩️ ${tableName}.${columnName} 제거`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -224,7 +213,6 @@ exports.down = async function(knex) {
|
||||
await knex.schema.table(tableName, (table) => {
|
||||
table.dropColumn('user_id');
|
||||
});
|
||||
console.log(`↩️ ${tableName}.user_id 제거`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ const { getDb } = require('../../dbPool');
|
||||
async function migrate() {
|
||||
const db = await getDb();
|
||||
|
||||
console.log('🔄 TBM 기반 작업보고서 work_type_id 수정 시작...\n');
|
||||
|
||||
try {
|
||||
// 1. 수정 대상 확인 (TBM 기반이면서 work_type_id가 task_id와 다른 경우)
|
||||
@@ -33,15 +32,12 @@ async function migrate() {
|
||||
ORDER BY dwr.report_date DESC
|
||||
`);
|
||||
|
||||
console.log(`📊 수정 대상: ${checkResult.length}개 레코드\n`);
|
||||
|
||||
if (checkResult.length === 0) {
|
||||
console.log('✅ 수정할 데이터가 없습니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
// 수정 대상 샘플 출력
|
||||
console.log('📋 수정 대상 샘플 (최대 10개):');
|
||||
console.log('─'.repeat(80));
|
||||
checkResult.slice(0, 10).forEach(row => {
|
||||
console.log(` ID: ${row.id} | ${row.worker_name} | ${row.report_date}`);
|
||||
@@ -62,7 +58,6 @@ async function migrate() {
|
||||
AND dwr.work_type_id != ta.task_id
|
||||
`);
|
||||
|
||||
console.log(`\n✅ 업데이트 완료: ${updateResult.affectedRows}개 레코드 수정됨`);
|
||||
|
||||
// 3. 수정 결과 확인
|
||||
const [verifyResult] = await db.query(`
|
||||
@@ -80,7 +75,6 @@ async function migrate() {
|
||||
LIMIT 5
|
||||
`);
|
||||
|
||||
console.log('\n📋 수정 후 샘플 확인:');
|
||||
console.log('─'.repeat(80));
|
||||
verifyResult.forEach(row => {
|
||||
console.log(` ID: ${row.id} | work_type_id: ${row.work_type_id} | task: ${row.task_name || 'N/A'} | 공정: ${row.work_type_name || 'N/A'}`);
|
||||
@@ -88,7 +82,7 @@ async function migrate() {
|
||||
console.log('─'.repeat(80));
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 마이그레이션 실패:', error.message);
|
||||
console.error(' 마이그레이션 실패:', error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -96,10 +90,9 @@ async function migrate() {
|
||||
// 실행
|
||||
migrate()
|
||||
.then(() => {
|
||||
console.log('\n🎉 마이그레이션 완료!');
|
||||
process.exit(0);
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('\n💥 마이그레이션 실패:', err);
|
||||
console.error('\n 마이그레이션 실패:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
@@ -48,18 +48,14 @@ const server = app.listen(PORT, () => {
|
||||
env: process.env.NODE_ENV || 'development',
|
||||
nodeVersion: process.version
|
||||
});
|
||||
console.log(`\n🚀 서버가 포트 ${PORT}에서 실행 중입니다.`);
|
||||
console.log(`📚 API 문서: http://localhost:${PORT}/api-docs\n`);
|
||||
});
|
||||
|
||||
// Graceful Shutdown
|
||||
const gracefulShutdown = (signal) => {
|
||||
logger.info(`${signal} 신호 수신 - 서버 종료 시작`);
|
||||
console.log(`\n🛑 ${signal} 신호를 받았습니다. 서버를 종료합니다...`);
|
||||
|
||||
server.close(async () => {
|
||||
logger.info('HTTP 서버 종료 완료');
|
||||
console.log('✅ HTTP 서버가 정상적으로 종료되었습니다.');
|
||||
|
||||
// 리소스 정리
|
||||
try {
|
||||
@@ -79,7 +75,7 @@ const gracefulShutdown = (signal) => {
|
||||
// 30초 후 강제 종료
|
||||
setTimeout(() => {
|
||||
logger.error('강제 종료 - 정상 종료 시간 초과');
|
||||
console.error('❌ 정상 종료 실패, 강제 종료합니다.');
|
||||
console.error(' 정상 종료 실패, 강제 종료합니다.');
|
||||
process.exit(1);
|
||||
}, 30000);
|
||||
};
|
||||
@@ -94,7 +90,7 @@ process.on('unhandledRejection', (reason, promise) => {
|
||||
reason: reason,
|
||||
promise: promise
|
||||
});
|
||||
console.error('⚠️ 처리되지 않은 Promise 거부:', reason);
|
||||
console.error(' 처리되지 않은 Promise 거부:', reason);
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
process.exit(1);
|
||||
@@ -107,7 +103,7 @@ process.on('uncaughtException', (error) => {
|
||||
error: error.message,
|
||||
stack: error.stack
|
||||
});
|
||||
console.error('💥 처리되지 않은 예외:', error);
|
||||
console.error(' 처리되지 않은 예외:', error);
|
||||
gracefulShutdown('UNCAUGHT_EXCEPTION');
|
||||
});
|
||||
|
||||
@@ -117,11 +113,10 @@ process.on('uncaughtException', (error) => {
|
||||
if (cache.initRedis) {
|
||||
await cache.initRedis();
|
||||
logger.info('캐시 시스템 초기화 완료');
|
||||
console.log('✅ 캐시 시스템 초기화 완료');
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn('캐시 시스템 초기화 실패 - 계속 진행', { error: error.message });
|
||||
console.warn('⚠️ 캐시 시스템 초기화 실패:', error.message);
|
||||
console.warn(' 캐시 시스템 초기화 실패:', error.message);
|
||||
}
|
||||
})();
|
||||
|
||||
|
||||
@@ -109,7 +109,6 @@ class MonthlyStatusModel {
|
||||
updatedCount++;
|
||||
}
|
||||
|
||||
console.log(`✅ ${year}년 ${month}월 집계 재계산 완료: ${updatedCount}건`);
|
||||
return { success: true, updatedCount };
|
||||
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,284 +0,0 @@
|
||||
const visitRequestModel = require('../models/visitRequestModel');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
// ==================== 출입 신청 관리 ====================
|
||||
|
||||
exports.createVisitRequest = async (req, res) => {
|
||||
try {
|
||||
const requester_id = req.user.user_id;
|
||||
const requestData = { requester_id, ...req.body };
|
||||
|
||||
const requiredFields = ['visitor_company', 'category_id', 'workplace_id', 'visit_date', 'visit_time', 'purpose_id'];
|
||||
for (const field of requiredFields) {
|
||||
if (!requestData[field]) {
|
||||
return res.status(400).json({ success: false, message: `${field}는 필수 입력 항목입니다.` });
|
||||
}
|
||||
}
|
||||
|
||||
const requestId = await visitRequestModel.createVisitRequest(requestData);
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: '출입 신청이 성공적으로 생성되었습니다.',
|
||||
data: { request_id: requestId }
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error('출입 신청 생성 오류:', err);
|
||||
res.status(500).json({ success: false, message: '출입 신청 생성 중 오류가 발생했습니다.' });
|
||||
}
|
||||
};
|
||||
|
||||
exports.getAllVisitRequests = async (req, res) => {
|
||||
try {
|
||||
const filters = {
|
||||
status: req.query.status,
|
||||
visit_date: req.query.visit_date,
|
||||
start_date: req.query.start_date,
|
||||
end_date: req.query.end_date,
|
||||
requester_id: req.query.requester_id,
|
||||
category_id: req.query.category_id
|
||||
};
|
||||
|
||||
const requests = await visitRequestModel.getAllVisitRequests(filters);
|
||||
res.json({ success: true, data: requests });
|
||||
} catch (err) {
|
||||
logger.error('출입 신청 목록 조회 오류:', err);
|
||||
res.status(500).json({ success: false, message: '출입 신청 목록 조회 중 오류가 발생했습니다.' });
|
||||
}
|
||||
};
|
||||
|
||||
exports.getVisitRequestById = async (req, res) => {
|
||||
try {
|
||||
const request = await visitRequestModel.getVisitRequestById(req.params.id);
|
||||
if (!request) {
|
||||
return res.status(404).json({ success: false, message: '출입 신청을 찾을 수 없습니다.' });
|
||||
}
|
||||
res.json({ success: true, data: request });
|
||||
} catch (err) {
|
||||
logger.error('출입 신청 조회 오류:', err);
|
||||
res.status(500).json({ success: false, message: '출입 신청 조회 중 오류가 발생했습니다.' });
|
||||
}
|
||||
};
|
||||
|
||||
exports.updateVisitRequest = async (req, res) => {
|
||||
try {
|
||||
const result = await visitRequestModel.updateVisitRequest(req.params.id, req.body);
|
||||
if (result.affectedRows === 0) {
|
||||
return res.status(404).json({ success: false, message: '출입 신청을 찾을 수 없습니다.' });
|
||||
}
|
||||
res.json({ success: true, message: '출입 신청이 수정되었습니다.' });
|
||||
} catch (err) {
|
||||
logger.error('출입 신청 수정 오류:', err);
|
||||
res.status(500).json({ success: false, message: '출입 신청 수정 중 오류가 발생했습니다.' });
|
||||
}
|
||||
};
|
||||
|
||||
exports.deleteVisitRequest = async (req, res) => {
|
||||
try {
|
||||
const result = await visitRequestModel.deleteVisitRequest(req.params.id);
|
||||
if (result.affectedRows === 0) {
|
||||
return res.status(404).json({ success: false, message: '출입 신청을 찾을 수 없습니다.' });
|
||||
}
|
||||
res.json({ success: true, message: '출입 신청이 삭제되었습니다.' });
|
||||
} catch (err) {
|
||||
logger.error('출입 신청 삭제 오류:', err);
|
||||
res.status(500).json({ success: false, message: '출입 신청 삭제 중 오류가 발생했습니다.' });
|
||||
}
|
||||
};
|
||||
|
||||
exports.approveVisitRequest = async (req, res) => {
|
||||
try {
|
||||
const result = await visitRequestModel.approveVisitRequest(req.params.id, req.user.user_id);
|
||||
if (result.affectedRows === 0) {
|
||||
return res.status(404).json({ success: false, message: '출입 신청을 찾을 수 없습니다.' });
|
||||
}
|
||||
res.json({ success: true, message: '출입 신청이 승인되었습니다.' });
|
||||
} catch (err) {
|
||||
logger.error('출입 신청 승인 오류:', err);
|
||||
res.status(500).json({ success: false, message: '출입 신청 승인 중 오류가 발생했습니다.' });
|
||||
}
|
||||
};
|
||||
|
||||
exports.rejectVisitRequest = async (req, res) => {
|
||||
try {
|
||||
const rejectionData = {
|
||||
approved_by: req.user.user_id,
|
||||
rejection_reason: req.body.rejection_reason || '사유 없음'
|
||||
};
|
||||
const result = await visitRequestModel.rejectVisitRequest(req.params.id, rejectionData);
|
||||
if (result.affectedRows === 0) {
|
||||
return res.status(404).json({ success: false, message: '출입 신청을 찾을 수 없습니다.' });
|
||||
}
|
||||
res.json({ success: true, message: '출입 신청이 반려되었습니다.' });
|
||||
} catch (err) {
|
||||
logger.error('출입 신청 반려 오류:', err);
|
||||
res.status(500).json({ success: false, message: '출입 신청 반려 중 오류가 발생했습니다.' });
|
||||
}
|
||||
};
|
||||
|
||||
// ==================== 방문 목적 관리 ====================
|
||||
|
||||
exports.getAllVisitPurposes = async (req, res) => {
|
||||
try {
|
||||
const purposes = await visitRequestModel.getAllVisitPurposes();
|
||||
res.json({ success: true, data: purposes });
|
||||
} catch (err) {
|
||||
logger.error('방문 목적 조회 오류:', err);
|
||||
res.status(500).json({ success: false, message: '방문 목적 조회 중 오류가 발생했습니다.' });
|
||||
}
|
||||
};
|
||||
|
||||
exports.getActiveVisitPurposes = async (req, res) => {
|
||||
try {
|
||||
const purposes = await visitRequestModel.getActiveVisitPurposes();
|
||||
res.json({ success: true, data: purposes });
|
||||
} catch (err) {
|
||||
logger.error('활성 방문 목적 조회 오류:', err);
|
||||
res.status(500).json({ success: false, message: '활성 방문 목적 조회 중 오류가 발생했습니다.' });
|
||||
}
|
||||
};
|
||||
|
||||
exports.createVisitPurpose = async (req, res) => {
|
||||
try {
|
||||
if (!req.body.purpose_name) {
|
||||
return res.status(400).json({ success: false, message: 'purpose_name은 필수 입력 항목입니다.' });
|
||||
}
|
||||
const purposeId = await visitRequestModel.createVisitPurpose(req.body);
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: '방문 목적이 추가되었습니다.',
|
||||
data: { purpose_id: purposeId }
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error('방문 목적 추가 오류:', err);
|
||||
res.status(500).json({ success: false, message: '방문 목적 추가 중 오류가 발생했습니다.' });
|
||||
}
|
||||
};
|
||||
|
||||
exports.updateVisitPurpose = async (req, res) => {
|
||||
try {
|
||||
const result = await visitRequestModel.updateVisitPurpose(req.params.id, req.body);
|
||||
if (result.affectedRows === 0) {
|
||||
return res.status(404).json({ success: false, message: '방문 목적을 찾을 수 없습니다.' });
|
||||
}
|
||||
res.json({ success: true, message: '방문 목적이 수정되었습니다.' });
|
||||
} catch (err) {
|
||||
logger.error('방문 목적 수정 오류:', err);
|
||||
res.status(500).json({ success: false, message: '방문 목적 수정 중 오류가 발생했습니다.' });
|
||||
}
|
||||
};
|
||||
|
||||
exports.deleteVisitPurpose = async (req, res) => {
|
||||
try {
|
||||
const result = await visitRequestModel.deleteVisitPurpose(req.params.id);
|
||||
if (result.affectedRows === 0) {
|
||||
return res.status(404).json({ success: false, message: '방문 목적을 찾을 수 없습니다.' });
|
||||
}
|
||||
res.json({ success: true, message: '방문 목적이 삭제되었습니다.' });
|
||||
} catch (err) {
|
||||
logger.error('방문 목적 삭제 오류:', err);
|
||||
res.status(500).json({ success: false, message: '방문 목적 삭제 중 오류가 발생했습니다.' });
|
||||
}
|
||||
};
|
||||
|
||||
// ==================== 안전교육 기록 관리 ====================
|
||||
|
||||
exports.createTrainingRecord = async (req, res) => {
|
||||
try {
|
||||
const trainingData = { trainer_id: req.user.user_id, ...req.body };
|
||||
|
||||
const requiredFields = ['request_id', 'training_date', 'training_start_time'];
|
||||
for (const field of requiredFields) {
|
||||
if (!trainingData[field]) {
|
||||
return res.status(400).json({ success: false, message: `${field}는 필수 입력 항목입니다.` });
|
||||
}
|
||||
}
|
||||
|
||||
const trainingId = await visitRequestModel.createTrainingRecord(trainingData);
|
||||
|
||||
// 안전교육 기록 생성 후 출입 신청 상태를 training_completed로 변경
|
||||
try {
|
||||
await visitRequestModel.updateVisitRequestStatus(trainingData.request_id, 'training_completed');
|
||||
} catch (statusErr) {
|
||||
logger.error('출입 신청 상태 업데이트 오류:', statusErr);
|
||||
}
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: '안전교육 기록이 생성되었습니다.',
|
||||
data: { training_id: trainingId }
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error('안전교육 기록 생성 오류:', err);
|
||||
res.status(500).json({ success: false, message: '안전교육 기록 생성 중 오류가 발생했습니다.' });
|
||||
}
|
||||
};
|
||||
|
||||
exports.getTrainingRecordByRequestId = async (req, res) => {
|
||||
try {
|
||||
const record = await visitRequestModel.getTrainingRecordByRequestId(req.params.requestId);
|
||||
res.json({ success: true, data: record || null });
|
||||
} catch (err) {
|
||||
logger.error('안전교육 기록 조회 오류:', err);
|
||||
res.status(500).json({ success: false, message: '안전교육 기록 조회 중 오류가 발생했습니다.' });
|
||||
}
|
||||
};
|
||||
|
||||
exports.updateTrainingRecord = async (req, res) => {
|
||||
try {
|
||||
const result = await visitRequestModel.updateTrainingRecord(req.params.id, req.body);
|
||||
if (result.affectedRows === 0) {
|
||||
return res.status(404).json({ success: false, message: '안전교육 기록을 찾을 수 없습니다.' });
|
||||
}
|
||||
res.json({ success: true, message: '안전교육 기록이 수정되었습니다.' });
|
||||
} catch (err) {
|
||||
logger.error('안전교육 기록 수정 오류:', err);
|
||||
res.status(500).json({ success: false, message: '안전교육 기록 수정 중 오류가 발생했습니다.' });
|
||||
}
|
||||
};
|
||||
|
||||
exports.completeTraining = async (req, res) => {
|
||||
try {
|
||||
const trainingId = req.params.id;
|
||||
const signatureData = req.body.signature_data;
|
||||
|
||||
if (!signatureData) {
|
||||
return res.status(400).json({ success: false, message: '서명 데이터가 필요합니다.' });
|
||||
}
|
||||
|
||||
const result = await visitRequestModel.completeTraining(trainingId, signatureData);
|
||||
if (result.affectedRows === 0) {
|
||||
return res.status(404).json({ success: false, message: '안전교육 기록을 찾을 수 없습니다.' });
|
||||
}
|
||||
|
||||
// 교육 완료 후 출입 신청 상태 변경
|
||||
try {
|
||||
const record = await visitRequestModel.getTrainingRecordByRequestId(trainingId);
|
||||
if (record) {
|
||||
await visitRequestModel.updateVisitRequestStatus(record.request_id, 'training_completed');
|
||||
}
|
||||
} catch (statusErr) {
|
||||
logger.error('출입 신청 상태 업데이트 오류:', statusErr);
|
||||
}
|
||||
|
||||
res.json({ success: true, message: '안전교육이 완료되었습니다.' });
|
||||
} catch (err) {
|
||||
logger.error('안전교육 완료 처리 오류:', err);
|
||||
res.status(500).json({ success: false, message: '안전교육 완료 처리 중 오류가 발생했습니다.' });
|
||||
}
|
||||
};
|
||||
|
||||
exports.getTrainingRecords = async (req, res) => {
|
||||
try {
|
||||
const filters = {
|
||||
training_date: req.query.training_date,
|
||||
start_date: req.query.start_date,
|
||||
end_date: req.query.end_date,
|
||||
trainer_id: req.query.trainer_id
|
||||
};
|
||||
const records = await visitRequestModel.getTrainingRecords(filters);
|
||||
res.json({ success: true, data: records });
|
||||
} catch (err) {
|
||||
logger.error('안전교육 기록 목록 조회 오류:', err);
|
||||
res.status(500).json({ success: false, message: '안전교육 기록 목록 조회 중 오류가 발생했습니다.' });
|
||||
}
|
||||
};
|
||||
@@ -174,7 +174,6 @@ const remove = async (userId) => {
|
||||
try {
|
||||
await conn.beginTransaction();
|
||||
|
||||
console.log(`🗑️ 작업자 삭제 시작: user_id=${userId}`);
|
||||
|
||||
// 안전한 삭제: 각 테이블을 개별적으로 처리하고 오류가 발생해도 계속 진행
|
||||
const tables = [
|
||||
@@ -195,10 +194,8 @@ const remove = async (userId) => {
|
||||
try {
|
||||
const [result] = await conn.query(table.query, [userId]);
|
||||
if (result.affectedRows > 0) {
|
||||
console.log(`✅ ${table.name} 테이블 ${table.action}: ${result.affectedRows}건`);
|
||||
}
|
||||
} catch (tableError) {
|
||||
console.log(`⚠️ ${table.name} 테이블 ${table.action} 실패 (무시): ${tableError.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,14 +204,13 @@ const remove = async (userId) => {
|
||||
`DELETE FROM workers WHERE user_id = ?`,
|
||||
[userId]
|
||||
);
|
||||
console.log(`✅ 작업자 삭제 완료: ${result.affectedRows}건`);
|
||||
|
||||
await conn.commit();
|
||||
return result.affectedRows;
|
||||
|
||||
} catch (err) {
|
||||
await conn.rollback();
|
||||
console.error(`❌ 작업자 삭제 오류 (user_id: ${userId}):`, err);
|
||||
console.error(` 작업자 삭제 오류 (user_id: ${userId}):`, err);
|
||||
throw new Error(`작업자 삭제 중 오류가 발생했습니다: ${err.message}`);
|
||||
} finally {
|
||||
conn.release();
|
||||
|
||||
@@ -9,20 +9,12 @@
|
||||
const express = require('express');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const mysql = require('mysql2/promise');
|
||||
const { getDb } = require('../dbPool');
|
||||
const { verifyToken } = require('../middlewares/auth');
|
||||
const { validatePassword, getPasswordError } = require('../utils/passwordValidator');
|
||||
const router = express.Router();
|
||||
const authController = require('../controllers/authController');
|
||||
|
||||
// DB 연결 설정
|
||||
const dbConfig = {
|
||||
host: process.env.DB_HOST || 'db_hyungi_net',
|
||||
user: process.env.DB_USER || 'hyungi',
|
||||
password: process.env.DB_PASSWORD,
|
||||
database: process.env.DB_NAME || 'hyungi'
|
||||
};
|
||||
|
||||
// 로그인 시도 추적 (메모리 기반 - 실제로는 Redis 권장)
|
||||
const loginAttempts = new Map();
|
||||
|
||||
@@ -143,7 +135,7 @@ router.post('/refresh-token', async (req, res) => {
|
||||
return res.status(401).json({ error: '유효하지 않은 토큰입니다.' });
|
||||
}
|
||||
|
||||
const connection = await mysql.createConnection(dbConfig);
|
||||
const connection = await getDb();
|
||||
|
||||
// 사용자 정보 조회
|
||||
const [users] = await connection.execute(
|
||||
@@ -151,8 +143,6 @@ router.post('/refresh-token', async (req, res) => {
|
||||
[decoded.user_id]
|
||||
);
|
||||
|
||||
await connection.end();
|
||||
|
||||
if (users.length === 0) {
|
||||
return res.status(404).json({ error: '사용자를 찾을 수 없습니다.' });
|
||||
}
|
||||
@@ -167,7 +157,7 @@ router.post('/refresh-token', async (req, res) => {
|
||||
access_level: user.access_level,
|
||||
name: user.name || user.username
|
||||
},
|
||||
process.env.JWT_SECRET || 'your-secret-key',
|
||||
process.env.JWT_SECRET,
|
||||
{ expiresIn: process.env.JWT_EXPIRES_IN || '24h' }
|
||||
);
|
||||
|
||||
@@ -224,7 +214,7 @@ router.post('/change-password', verifyToken, async (req, res) => {
|
||||
});
|
||||
}
|
||||
|
||||
connection = await mysql.createConnection(dbConfig);
|
||||
connection = await getDb();
|
||||
|
||||
// 현재 사용자의 비밀번호 조회
|
||||
const [users] = await connection.execute(
|
||||
@@ -290,10 +280,6 @@ router.post('/change-password', verifyToken, async (req, res) => {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.'
|
||||
});
|
||||
} finally {
|
||||
if (connection) {
|
||||
await connection.end();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -334,7 +320,7 @@ router.post('/admin/change-password', verifyToken, async (req, res) => {
|
||||
});
|
||||
}
|
||||
|
||||
connection = await mysql.createConnection(dbConfig);
|
||||
connection = await getDb();
|
||||
|
||||
// 대상 사용자 확인
|
||||
const [users] = await connection.execute(
|
||||
@@ -391,10 +377,6 @@ router.post('/admin/change-password', verifyToken, async (req, res) => {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.'
|
||||
});
|
||||
} finally {
|
||||
if (connection) {
|
||||
await connection.end();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -453,7 +435,7 @@ router.get('/me', verifyToken, async (req, res) => {
|
||||
try {
|
||||
const userId = req.user.user_id;
|
||||
|
||||
connection = await mysql.createConnection(dbConfig);
|
||||
connection = await getDb();
|
||||
const [rows] = await connection.execute(
|
||||
'SELECT user_id, username, name, email, access_level, last_login_at, created_at FROM users WHERE user_id = ?',
|
||||
[userId]
|
||||
@@ -477,10 +459,6 @@ router.get('/me', verifyToken, async (req, res) => {
|
||||
} catch (error) {
|
||||
console.error('Get current user error:', error);
|
||||
res.status(500).json({ error: '서버 오류가 발생했습니다.' });
|
||||
} finally {
|
||||
if (connection) {
|
||||
await connection.end();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -516,7 +494,7 @@ router.post('/register', verifyToken, async (req, res) => {
|
||||
});
|
||||
}
|
||||
|
||||
connection = await mysql.createConnection(dbConfig);
|
||||
connection = await getDb();
|
||||
|
||||
// 사용자명 중복 체크
|
||||
const [existing] = await connection.execute(
|
||||
@@ -586,10 +564,6 @@ router.post('/register', verifyToken, async (req, res) => {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.'
|
||||
});
|
||||
} finally {
|
||||
if (connection) {
|
||||
await connection.end();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -600,7 +574,7 @@ router.get('/users', verifyToken, async (req, res) => {
|
||||
let connection;
|
||||
|
||||
try {
|
||||
connection = await mysql.createConnection(dbConfig);
|
||||
connection = await getDb();
|
||||
|
||||
// 기본 쿼리 (role 테이블과 JOIN)
|
||||
let query = `
|
||||
@@ -656,10 +630,6 @@ router.get('/users', verifyToken, async (req, res) => {
|
||||
} catch (error) {
|
||||
console.error('Get users error:', error);
|
||||
res.status(500).json({ error: '서버 오류가 발생했습니다.' });
|
||||
} finally {
|
||||
if (connection) {
|
||||
await connection.end();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -691,7 +661,7 @@ router.put('/users/:id', verifyToken, async (req, res) => {
|
||||
}
|
||||
}
|
||||
|
||||
connection = await mysql.createConnection(dbConfig);
|
||||
connection = await getDb();
|
||||
|
||||
// 사용자 존재 확인
|
||||
const [existing] = await connection.execute(
|
||||
@@ -802,10 +772,6 @@ router.put('/users/:id', verifyToken, async (req, res) => {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.'
|
||||
});
|
||||
} finally {
|
||||
if (connection) {
|
||||
await connection.end();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -834,7 +800,7 @@ router.delete('/users/:id', verifyToken, async (req, res) => {
|
||||
});
|
||||
}
|
||||
|
||||
connection = await mysql.createConnection(dbConfig);
|
||||
connection = await getDb();
|
||||
|
||||
// 사용자 존재 확인
|
||||
const [existing] = await connection.execute(
|
||||
@@ -871,10 +837,6 @@ router.delete('/users/:id', verifyToken, async (req, res) => {
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.'
|
||||
});
|
||||
} finally {
|
||||
if (connection) {
|
||||
await connection.end();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -887,17 +849,13 @@ router.post('/logout', verifyToken, async (req, res) => {
|
||||
// 로그아웃 시간 기록 (선택사항)
|
||||
let connection;
|
||||
try {
|
||||
connection = await mysql.createConnection(dbConfig);
|
||||
connection = await getDb();
|
||||
await connection.execute(
|
||||
'UPDATE login_logs SET logout_time = NOW() WHERE user_id = ? AND logout_time IS NULL ORDER BY login_time DESC LIMIT 1',
|
||||
[req.user.user_id]
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('로그아웃 기록 실패:', error);
|
||||
} finally {
|
||||
if (connection) {
|
||||
await connection.end();
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
@@ -916,7 +874,7 @@ router.get('/login-history', verifyToken, async (req, res) => {
|
||||
const { limit = 50, offset = 0 } = req.query;
|
||||
const userId = req.user.user_id;
|
||||
|
||||
connection = await mysql.createConnection(dbConfig);
|
||||
connection = await getDb();
|
||||
|
||||
// 본인의 로그인 이력만 조회 (관리자는 전체 조회 가능)
|
||||
let query = `
|
||||
@@ -958,10 +916,6 @@ router.get('/login-history', verifyToken, async (req, res) => {
|
||||
} catch (error) {
|
||||
console.error('Get login history error:', error);
|
||||
res.status(500).json({ error: '서버 오류가 발생했습니다.' });
|
||||
} finally {
|
||||
if (connection) {
|
||||
await connection.end();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -41,7 +41,6 @@ router.get('/check-overwrite', (req, res) => {
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`🔍 덮어쓰기 권한 확인: 날짜=${date}, 작업자=${user_id} (누적입력모드)`);
|
||||
|
||||
// 누적입력 시스템에서는 항상 덮어쓰기 가능 (실제로는 누적만 함)
|
||||
res.json({
|
||||
|
||||
@@ -8,7 +8,6 @@ router.post('/setup-monthly-status', async (req, res) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
|
||||
console.log('📊 월별 집계 테이블 생성 중...');
|
||||
|
||||
// 1. 월별 작업자 상태 집계 테이블
|
||||
await db.execute(`
|
||||
@@ -86,7 +85,6 @@ router.post('/setup-monthly-status', async (req, res) => {
|
||||
) COMMENT='월별 일자별 요약 테이블 (캘린더 최적화용)'
|
||||
`);
|
||||
|
||||
console.log('📊 집계 프로시저 생성 중...');
|
||||
|
||||
// 3. 집계 업데이트 프로시저
|
||||
await db.execute(`DROP PROCEDURE IF EXISTS UpdateMonthlyWorkerStatus`);
|
||||
@@ -239,7 +237,6 @@ router.post('/setup-monthly-status', async (req, res) => {
|
||||
END
|
||||
`);
|
||||
|
||||
console.log('📊 트리거 생성 중...');
|
||||
|
||||
// 4. 트리거 생성
|
||||
await db.execute(`DROP TRIGGER IF EXISTS tr_daily_work_reports_insert`);
|
||||
@@ -276,7 +273,6 @@ router.post('/setup-monthly-status', async (req, res) => {
|
||||
END
|
||||
`);
|
||||
|
||||
console.log('📊 기존 데이터로 집계 테이블 초기화 중...');
|
||||
|
||||
// 5. 기존 작업 데이터로 집계 테이블 초기화
|
||||
const [existingDates] = await db.execute(`
|
||||
@@ -302,7 +298,6 @@ router.post('/setup-monthly-status', async (req, res) => {
|
||||
}
|
||||
|
||||
if (i % 100 === 0) {
|
||||
console.log(`📊 집계 초기화 진행률: ${processedCount}/${existingDates.length}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -329,7 +324,7 @@ router.post('/setup-monthly-status', async (req, res) => {
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 월별 집계 시스템 설정 오류:', error);
|
||||
console.error(' 월별 집계 시스템 설정 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '월별 집계 시스템 설정 중 오류가 발생했습니다.',
|
||||
@@ -340,12 +335,10 @@ router.post('/setup-monthly-status', async (req, res) => {
|
||||
|
||||
router.post('/setup-attendance-db', async (req, res) => {
|
||||
try {
|
||||
console.log('🚀 근태 관리 DB 설정 API 호출됨');
|
||||
|
||||
const db = await getDb();
|
||||
|
||||
// 1. 근로 유형 테이블 생성
|
||||
console.log('📋 근로 유형 테이블 생성 중...');
|
||||
await db.execute(`
|
||||
CREATE TABLE IF NOT EXISTS work_attendance_types (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
@@ -359,7 +352,6 @@ router.post('/setup-attendance-db', async (req, res) => {
|
||||
`);
|
||||
|
||||
// 2. 휴가 유형 테이블 생성
|
||||
console.log('🏖️ 휴가 유형 테이블 생성 중...');
|
||||
await db.execute(`
|
||||
CREATE TABLE IF NOT EXISTS vacation_types (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
@@ -374,7 +366,6 @@ router.post('/setup-attendance-db', async (req, res) => {
|
||||
`);
|
||||
|
||||
// 3. 일일 근태 기록 테이블 생성
|
||||
console.log('📊 일일 근태 기록 테이블 생성 중...');
|
||||
await db.execute(`
|
||||
CREATE TABLE IF NOT EXISTS daily_attendance_records (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
@@ -394,7 +385,6 @@ router.post('/setup-attendance-db', async (req, res) => {
|
||||
`);
|
||||
|
||||
// 4. 작업자별 휴가 잔여 관리 테이블 생성
|
||||
console.log('👥 작업자별 휴가 잔여 관리 테이블 생성 중...');
|
||||
await db.execute(`
|
||||
CREATE TABLE IF NOT EXISTS worker_vacation_balance (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
@@ -410,7 +400,6 @@ router.post('/setup-attendance-db', async (req, res) => {
|
||||
`);
|
||||
|
||||
// 5. 기본 데이터 삽입
|
||||
console.log('📝 기본 데이터 삽입 중...');
|
||||
|
||||
// 근로 유형 기본 데이터
|
||||
await db.execute(`
|
||||
@@ -446,7 +435,7 @@ router.post('/setup-attendance-db', async (req, res) => {
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ DB 설정 API 오류:', error);
|
||||
console.error(' DB 설정 API 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'DB 설정 중 오류가 발생했습니다.',
|
||||
@@ -460,7 +449,6 @@ router.post('/add-overtime-warning', async (req, res) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
|
||||
console.log('⚠️ 12시간 초과 상태 컬럼 추가 중...');
|
||||
|
||||
// 1. monthly_summary 테이블에 컬럼 추가
|
||||
try {
|
||||
@@ -468,10 +456,8 @@ router.post('/add-overtime-warning', async (req, res) => {
|
||||
ALTER TABLE monthly_summary
|
||||
ADD COLUMN overtime_warning_workers INT DEFAULT 0 COMMENT '확인필요(12시간초과) 작업자 수' AFTER error_workers
|
||||
`);
|
||||
console.log('✅ overtime_warning_workers 컬럼 추가 완료');
|
||||
} catch (error) {
|
||||
if (error.code === 'ER_DUP_FIELDNAME') {
|
||||
console.log('ℹ️ overtime_warning_workers 컬럼이 이미 존재합니다.');
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
@@ -482,10 +468,8 @@ router.post('/add-overtime-warning', async (req, res) => {
|
||||
ALTER TABLE monthly_summary
|
||||
ADD COLUMN has_overtime_warning BOOLEAN DEFAULT FALSE COMMENT '확인필요 상태 있음' AFTER has_errors
|
||||
`);
|
||||
console.log('✅ has_overtime_warning 컬럼 추가 완료');
|
||||
} catch (error) {
|
||||
if (error.code === 'ER_DUP_FIELDNAME') {
|
||||
console.log('ℹ️ has_overtime_warning 컬럼이 이미 존재합니다.');
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
@@ -551,7 +535,6 @@ router.post('/add-overtime-warning', async (req, res) => {
|
||||
last_updated = CURRENT_TIMESTAMP;
|
||||
END
|
||||
`);
|
||||
console.log('✅ UpdateDailySummary 프로시저 업데이트 완료');
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
@@ -561,7 +544,7 @@ router.post('/add-overtime-warning', async (req, res) => {
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 12시간 초과 상태 설정 오류:', error);
|
||||
console.error(' 12시간 초과 상태 설정 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '12시간 초과 상태 설정 실패',
|
||||
@@ -575,7 +558,6 @@ router.post('/migrate-existing-data', async (req, res) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
|
||||
console.log('🔄 기존 데이터 마이그레이션 시작...');
|
||||
|
||||
// 1. 기존 데이터 범위 확인
|
||||
const [dateRange] = await db.execute(`
|
||||
@@ -595,12 +577,10 @@ router.post('/migrate-existing-data', async (req, res) => {
|
||||
}
|
||||
|
||||
const { min_date, max_date, total_reports } = dateRange[0];
|
||||
console.log(`📊 데이터 범위: ${min_date} ~ ${max_date} (총 ${total_reports}건)`);
|
||||
|
||||
// 2. 기존 monthly_worker_status, monthly_summary 데이터 삭제
|
||||
await db.execute('DELETE FROM monthly_summary');
|
||||
await db.execute('DELETE FROM monthly_worker_status');
|
||||
console.log('🗑️ 기존 집계 데이터 삭제 완료');
|
||||
|
||||
// 3. 날짜별로 작업자별 상태 재계산
|
||||
const [allDates] = await db.execute(`
|
||||
@@ -610,7 +590,6 @@ router.post('/migrate-existing-data', async (req, res) => {
|
||||
ORDER BY report_date, worker_id
|
||||
`, [min_date, max_date]);
|
||||
|
||||
console.log(`🔄 ${allDates.length}개 날짜-작업자 조합 처리 중...`);
|
||||
|
||||
let processedCount = 0;
|
||||
for (const { report_date, worker_id } of allDates) {
|
||||
@@ -620,10 +599,9 @@ router.post('/migrate-existing-data', async (req, res) => {
|
||||
processedCount++;
|
||||
|
||||
if (processedCount % 50 === 0) {
|
||||
console.log(`📈 진행률: ${processedCount}/${allDates.length} (${Math.round(processedCount/allDates.length*100)}%)`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`❌ ${report_date} ${worker_id} 처리 오류:`, error.message);
|
||||
console.error(` ${report_date} ${worker_id} 처리 오류:`, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -631,7 +609,6 @@ router.post('/migrate-existing-data', async (req, res) => {
|
||||
const [workerStatusCount] = await db.execute('SELECT COUNT(*) as count FROM monthly_worker_status');
|
||||
const [summaryCount] = await db.execute('SELECT COUNT(*) as count FROM monthly_summary');
|
||||
|
||||
console.log(`✅ 마이그레이션 완료:`);
|
||||
console.log(` - monthly_worker_status: ${workerStatusCount[0].count}건`);
|
||||
console.log(` - monthly_summary: ${summaryCount[0].count}건`);
|
||||
|
||||
@@ -649,7 +626,7 @@ router.post('/migrate-existing-data', async (req, res) => {
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 데이터 마이그레이션 오류:', error);
|
||||
console.error(' 데이터 마이그레이션 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '데이터 마이그레이션 실패',
|
||||
@@ -705,7 +682,7 @@ router.get('/check-data-status', async (req, res) => {
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ DB 상태 확인 오류:', error);
|
||||
console.error(' DB 상태 확인 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'DB 상태 확인 실패',
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const systemController = require('../controllers/systemController');
|
||||
const userController = require('../controllers/userController');
|
||||
const { requireAuth, requireRole } = require('../middlewares/auth');
|
||||
|
||||
// 모든 라우트에 인증 및 시스템 권한 확인 적용
|
||||
@@ -46,31 +47,31 @@ router.get('/users/stats', systemController.getUserStats);
|
||||
* GET /api/system/users
|
||||
* 모든 사용자 목록 조회
|
||||
*/
|
||||
router.get('/users', systemController.getAllUsers);
|
||||
router.get('/users', userController.getAllUsers);
|
||||
|
||||
/**
|
||||
* POST /api/system/users
|
||||
* 새 사용자 생성
|
||||
*/
|
||||
router.post('/users', systemController.createUser);
|
||||
router.post('/users', userController.createUser);
|
||||
|
||||
/**
|
||||
* PUT /api/system/users/:id
|
||||
* 사용자 정보 수정
|
||||
*/
|
||||
router.put('/users/:id', systemController.updateUser);
|
||||
router.put('/users/:id', userController.updateUser);
|
||||
|
||||
/**
|
||||
* DELETE /api/system/users/:id
|
||||
* 사용자 삭제
|
||||
*/
|
||||
router.delete('/users/:id', systemController.deleteUser);
|
||||
router.delete('/users/:id', userController.deleteUser);
|
||||
|
||||
/**
|
||||
* POST /api/system/users/:id/reset-password
|
||||
* 사용자 비밀번호 재설정
|
||||
*/
|
||||
router.post('/users/:id/reset-password', systemController.resetUserPassword);
|
||||
router.post('/users/:id/reset-password', userController.resetUserPassword);
|
||||
|
||||
// ===== 시스템 로그 관련 =====
|
||||
|
||||
@@ -219,7 +220,6 @@ router.post('/migrations/fix-work-type-id', async (req, res) => {
|
||||
const { getDb } = require('../dbPool');
|
||||
const db = await getDb();
|
||||
|
||||
console.log('🔄 TBM 기반 작업보고서 work_type_id 수정 시작...');
|
||||
|
||||
// 1. 수정 대상 확인
|
||||
const [checkResult] = await db.query(`
|
||||
@@ -256,7 +256,6 @@ router.post('/migrations/fix-work-type-id', async (req, res) => {
|
||||
AND dwr.work_type_id != ta.task_id
|
||||
`);
|
||||
|
||||
console.log(`✅ 업데이트 완료: ${updateResult.affectedRows}개 레코드 수정됨`);
|
||||
|
||||
// 3. 수정된 샘플 조회
|
||||
const [samples] = await db.query(`
|
||||
|
||||
@@ -56,15 +56,19 @@ const loginService = async (username, password, ipAddress, userAgent) => {
|
||||
await userModel.resetLoginAttempts(user.user_id);
|
||||
|
||||
|
||||
if (!process.env.JWT_SECRET) {
|
||||
throw new Error('JWT_SECRET 환경변수가 설정되지 않았습니다');
|
||||
}
|
||||
|
||||
const token = jwt.sign(
|
||||
{ user_id: user.user_id, username: user.username, role: user.role_name, role_id: user.role_id, access_level: user.access_level, name: user.name || user.username },
|
||||
process.env.JWT_SECRET || 'your-secret-key',
|
||||
process.env.JWT_SECRET,
|
||||
{ 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',
|
||||
process.env.JWT_REFRESH_SECRET || process.env.JWT_SECRET,
|
||||
{ expiresIn: process.env.JWT_REFRESH_EXPIRES_IN || '7d' }
|
||||
);
|
||||
|
||||
|
||||
@@ -4,12 +4,10 @@ const path = require('path');
|
||||
|
||||
async function setupAttendanceDB() {
|
||||
try {
|
||||
console.log('🚀 근태 관리 DB 설정 시작...');
|
||||
|
||||
const db = await getDb();
|
||||
|
||||
// 1. 근로 유형 테이블 생성
|
||||
console.log('📋 근로 유형 테이블 생성 중...');
|
||||
await db.execute(`
|
||||
CREATE TABLE IF NOT EXISTS work_attendance_types (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
@@ -23,7 +21,6 @@ async function setupAttendanceDB() {
|
||||
`);
|
||||
|
||||
// 2. 휴가 유형 테이블 생성
|
||||
console.log('🏖️ 휴가 유형 테이블 생성 중...');
|
||||
await db.execute(`
|
||||
CREATE TABLE IF NOT EXISTS vacation_types (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
@@ -38,7 +35,6 @@ async function setupAttendanceDB() {
|
||||
`);
|
||||
|
||||
// 3. 일일 근태 기록 테이블 생성
|
||||
console.log('📊 일일 근태 기록 테이블 생성 중...');
|
||||
await db.execute(`
|
||||
CREATE TABLE IF NOT EXISTS daily_attendance_records (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
@@ -66,7 +62,6 @@ async function setupAttendanceDB() {
|
||||
`);
|
||||
|
||||
// 4. 작업자 휴가 잔여 관리 테이블 생성
|
||||
console.log('📅 휴가 잔여 관리 테이블 생성 중...');
|
||||
await db.execute(`
|
||||
CREATE TABLE IF NOT EXISTS worker_vacation_balance (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
@@ -85,7 +80,6 @@ async function setupAttendanceDB() {
|
||||
`);
|
||||
|
||||
// 5. 기본 데이터 삽입
|
||||
console.log('📝 기본 데이터 삽입 중...');
|
||||
|
||||
// 근로 유형 기본 데이터
|
||||
await db.execute(`
|
||||
@@ -109,7 +103,6 @@ async function setupAttendanceDB() {
|
||||
`);
|
||||
|
||||
// 6. 휴가 전용 작업 유형 추가
|
||||
console.log('🏖️ 휴가 전용 작업 유형 추가 중...');
|
||||
await db.execute(`
|
||||
INSERT IGNORE INTO work_types (name, description, is_active) VALUES
|
||||
('휴가', '연차, 반차, 병가 등 휴가 처리용', TRUE)
|
||||
@@ -121,42 +114,32 @@ async function setupAttendanceDB() {
|
||||
ALTER TABLE daily_work_reports
|
||||
ADD COLUMN attendance_record_id INT NULL COMMENT '근태 기록 ID' AFTER updated_by
|
||||
`);
|
||||
console.log('✅ daily_work_reports 테이블에 attendance_record_id 컬럼 추가됨');
|
||||
} catch (error) {
|
||||
if (error.code !== 'ER_DUP_FIELDNAME') {
|
||||
console.log('⚠️ attendance_record_id 컬럼 추가 실패 (이미 존재할 수 있음):', error.message);
|
||||
} else {
|
||||
console.log('✅ attendance_record_id 컬럼이 이미 존재함');
|
||||
}
|
||||
}
|
||||
|
||||
// 8. 인덱스 추가
|
||||
try {
|
||||
await db.execute(`CREATE INDEX idx_attendance_record ON daily_work_reports(attendance_record_id)`);
|
||||
console.log('✅ attendance_record_id 인덱스 추가됨');
|
||||
} catch (error) {
|
||||
console.log('⚠️ 인덱스 추가 실패 (이미 존재할 수 있음):', error.message);
|
||||
}
|
||||
|
||||
try {
|
||||
await db.execute(`CREATE INDEX idx_daily_work_reports_worker_date ON daily_work_reports(worker_id, report_date)`);
|
||||
console.log('✅ worker_date 인덱스 추가됨');
|
||||
} catch (error) {
|
||||
console.log('⚠️ 인덱스 추가 실패 (이미 존재할 수 있음):', error.message);
|
||||
}
|
||||
|
||||
console.log('🎉 근태 관리 DB 설정 완료!');
|
||||
console.log('');
|
||||
console.log('📋 생성된 테이블:');
|
||||
console.log(' - work_attendance_types (근로 유형)');
|
||||
console.log(' - vacation_types (휴가 유형)');
|
||||
console.log(' - daily_attendance_records (일일 근태 기록)');
|
||||
console.log(' - worker_vacation_balance (휴가 잔여 관리)');
|
||||
console.log('');
|
||||
console.log('✅ 기본 데이터도 모두 삽입되었습니다.');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ DB 설정 중 오류 발생:', error);
|
||||
console.error(' DB 설정 중 오류 발생:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -165,11 +148,10 @@ async function setupAttendanceDB() {
|
||||
if (require.main === module) {
|
||||
setupAttendanceDB()
|
||||
.then(() => {
|
||||
console.log('✅ 설정 완료');
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('❌ 설정 실패:', error);
|
||||
console.error(' 설정 실패:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -41,7 +41,6 @@ const initRedis = async () => {
|
||||
});
|
||||
|
||||
redisClient.on('connect', () => {
|
||||
console.log('✅ Redis 캐시 연결 성공');
|
||||
});
|
||||
|
||||
await redisClient.connect();
|
||||
@@ -200,7 +199,6 @@ const createCacheMiddleware = (keyGenerator, ttl = TTL.MEDIUM) => {
|
||||
const cachedData = await get(cacheKey);
|
||||
|
||||
if (cachedData) {
|
||||
console.log(`🎯 캐시 히트: ${cacheKey}`);
|
||||
return res.json(cachedData);
|
||||
}
|
||||
|
||||
@@ -212,7 +210,6 @@ const createCacheMiddleware = (keyGenerator, ttl = TTL.MEDIUM) => {
|
||||
// 성공 응답만 캐시
|
||||
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||||
set(cacheKey, data, ttl).then(() => {
|
||||
console.log(`💾 캐시 저장: ${cacheKey}`);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user