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:
Hyungi Ahn
2026-03-06 23:18:00 +09:00
parent 2f7e083db0
commit ec755ed52f
47 changed files with 181 additions and 716 deletions

View File

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

View File

@@ -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
});
// 로그인 시도 제한 (브루트포스 방지)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 컬럼 삭제 완료');
};

View File

@@ -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 테이블 컬럼 제거 완료');
};

View File

@@ -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('✅ 모든 출근/근태 관련 테이블 제거 완료');
};

View File

@@ -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 테이블을 관리하세요.');
};

View File

@@ -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('✅ 게스트 역할 제거 완료');
};

View File

@@ -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 시스템 테이블 제거 완료');
};

View File

@@ -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 페이지 제거 완료');
};

View File

@@ -24,7 +24,6 @@ exports.up = function(knex) {
table.index('work_type_id');
table.index('is_active');
}).then(() => {
console.log('✅ tasks 테이블 생성 완료');
});
};

View File

@@ -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 컬럼 추가 완료');
});
};

View File

@@ -143,10 +143,8 @@ exports.up = async function(knex) {
}
]);
console.log('✅ 현재 사용 중인 페이지 목록 업데이트 완료');
};
exports.down = async function(knex) {
await knex('pages').del();
console.log('✅ 페이지 목록 삭제 완료');
};

View File

@@ -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 컬럼 제거 완료');
};

View File

@@ -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 테이블 삭제 완료');
};

View File

@@ -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 테이블 삭제 완료');
};

View File

@@ -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('✅ 출퇴근 관리 페이지 삭제 완료');
};

View File

@@ -18,9 +18,7 @@ exports.up = async function(knex) {
.whereNotNull('id')
.update({ is_present: true });
console.log('✅ is_present 컬럼 추가 완료');
} else {
console.log('⏭️ is_present 컬럼이 이미 존재합니다');
}
};

View File

@@ -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('✅ 휴가 관리 페이지 롤백 완료');
};

View File

@@ -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 테이블 롤백 완료');
};

View File

@@ -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 테이블 롤백 완료');
};

View File

@@ -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('✅ 휴가 관리 페이지 롤백 완료');
};

View File

@@ -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('✅ 출입 신청 및 안전교육 시스템 테이블 삭제 완료');
};

View File

@@ -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('✅ 출입 신청 및 안전관리 페이지 삭제 완료');
};

View File

@@ -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('✅ 문제 신고 페이지 삭제 완료');
};

View File

@@ -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 컬럼 삭제 완료');
};

View File

@@ -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('✅ 모든 일일순회점검 시스템 테이블 제거 완료');
};

View File

@@ -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(' 데이터 복구가 필요한 경우 백업에서 복원해주세요.');
};

View File

@@ -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 컬럼이 이미 존재합니다');
}
};

View File

@@ -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 제거`);
}
}
}

View File

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

View File

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

View File

@@ -109,7 +109,6 @@ class MonthlyStatusModel {
updatedCount++;
}
console.log(`${year}${month}월 집계 재계산 완료: ${updatedCount}`);
return { success: true, updatedCount };
} catch (error) {

View File

@@ -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: '안전교육 기록 목록 조회 중 오류가 발생했습니다.' });
}
};

View File

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

View File

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

View File

@@ -41,7 +41,6 @@ router.get('/check-overwrite', (req, res) => {
});
}
console.log(`🔍 덮어쓰기 권한 확인: 날짜=${date}, 작업자=${user_id} (누적입력모드)`);
// 누적입력 시스템에서는 항상 덮어쓰기 가능 (실제로는 누적만 함)
res.json({

View File

@@ -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 상태 확인 실패',

View File

@@ -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(`

View File

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

View File

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

View File

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