refactor: worker_id → user_id 전체 마이그레이션 (Phase 1-4)

sso_users.user_id를 단일 식별자로 통합. JWT에서 worker_id 제거,
department_id/is_production 추가. 백엔드 15개 모델, 11개 컨트롤러,
4개 서비스, 7개 라우트, 프론트엔드 32+ JS/11+ HTML 변환.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Hyungi Ahn
2026-03-05 13:13:10 +09:00
parent 2197cdb3d5
commit abd7564e6b
90 changed files with 1790 additions and 925 deletions

View File

@@ -1,105 +0,0 @@
/**
* 마이그레이션: TBM 기반 작업보고서의 work_type_id를 task_id로 수정
*
* 문제: TBM에서 작업보고서 생성 시 work_type_id(공정 ID)가 저장됨
* 해결: tbm_team_assignments 테이블의 task_id로 업데이트
*
* 실행: node db/migrations/20260205_fix_work_type_id_data.js
*/
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와 다른 경우)
const [checkResult] = await db.query(`
SELECT
dwr.id,
dwr.work_type_id as current_work_type_id,
ta.task_id as correct_task_id,
ta.work_type_id as tbm_work_type_id,
w.worker_name,
dwr.report_date
FROM daily_work_reports dwr
INNER JOIN tbm_team_assignments ta ON dwr.tbm_assignment_id = ta.assignment_id
INNER JOIN workers w ON dwr.worker_id = w.worker_id
WHERE dwr.tbm_assignment_id IS NOT NULL
AND ta.task_id IS NOT NULL
AND dwr.work_type_id != ta.task_id
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}`);
console.log(` 현재 work_type_id: ${row.current_work_type_id} → 올바른 task_id: ${row.correct_task_id}`);
});
if (checkResult.length > 10) {
console.log(` ... 외 ${checkResult.length - 10}`);
}
console.log('─'.repeat(80));
// 2. 업데이트 실행
const [updateResult] = await db.query(`
UPDATE daily_work_reports dwr
INNER JOIN tbm_team_assignments ta ON dwr.tbm_assignment_id = ta.assignment_id
SET dwr.work_type_id = ta.task_id
WHERE dwr.tbm_assignment_id IS NOT NULL
AND ta.task_id IS NOT NULL
AND dwr.work_type_id != ta.task_id
`);
console.log(`\n✅ 업데이트 완료: ${updateResult.affectedRows}개 레코드 수정됨`);
// 3. 수정 결과 확인
const [verifyResult] = await db.query(`
SELECT
dwr.id,
dwr.work_type_id,
ta.task_id,
t.task_name,
wt.name as work_type_name
FROM daily_work_reports dwr
INNER JOIN tbm_team_assignments ta ON dwr.tbm_assignment_id = ta.assignment_id
LEFT JOIN tasks t ON dwr.work_type_id = t.task_id
LEFT JOIN work_types wt ON t.work_type_id = wt.id
WHERE dwr.tbm_assignment_id IS NOT NULL
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'}`);
});
console.log('─'.repeat(80));
} catch (error) {
console.error('❌ 마이그레이션 실패:', error.message);
throw error;
}
}
// 실행
migrate()
.then(() => {
console.log('\n🎉 마이그레이션 완료!');
process.exit(0);
})
.catch(err => {
console.error('\n💥 마이그레이션 실패:', err);
process.exit(1);
});

View File

@@ -0,0 +1,233 @@
/**
* worker_id → user_id 통합 마이그레이션 (Phase 1)
*
* 비파괴적 추가: 기존 worker_id 컬럼은 유지하면서 user_id 컬럼을 추가하고 백필.
* 코드 변경 전이므로 기존 시스템 동작에 영향 없음.
*
* 대상 테이블:
* 1. departments - is_production 플래그 추가
* 2. sso_users - department_id 추가 (없는 경우)
* 3. workers - user_id 추가 + 매핑 백필
* 4~15. 12개 참조 테이블 - user_id 추가 + 백필
*
* @since 2026-03-05
*/
exports.up = async function(knex) {
// ============================================================
// 1. departments 테이블에 is_production 플래그 추가
// ============================================================
const hasIsProduction = await knex.schema.hasColumn('departments', 'is_production');
if (!hasIsProduction) {
await knex.schema.table('departments', (table) => {
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 추가 완료');
}
// ============================================================
// 2. sso_users에 department_id 추가 (없는 경우)
// ============================================================
const hasSsoDeptId = await knex.schema.hasColumn('sso_users', 'department_id');
if (!hasSsoDeptId) {
await knex.schema.table('sso_users', (table) => {
table.integer('department_id').unsigned().defaultTo(null).comment('소속 부서 ID');
});
// 기존 department(문자열) → department_id(FK) 매핑
await knex.raw(`
UPDATE sso_users s
INNER JOIN departments d ON s.department = d.department_name
SET s.department_id = d.department_id
WHERE s.department IS NOT NULL
`);
console.log('✅ sso_users.department_id 추가 및 백필 완료');
}
// ============================================================
// 3. workers 테이블에 user_id 추가 + 매핑 백필
// ============================================================
const hasWorkersUserId = await knex.schema.hasColumn('workers', 'user_id');
if (!hasWorkersUserId) {
await knex.schema.table('workers', (table) => {
table.integer('user_id').unsigned().defaultTo(null).after('worker_id')
.comment('sso_users.user_id 매핑');
});
// users 테이블을 경유하여 sso_users.user_id 매핑
await knex.raw(`
UPDATE workers w
INNER JOIN users u ON u.worker_id = w.worker_id
INNER JOIN sso_users s ON s.username = u.username
SET w.user_id = s.user_id
`);
// user_id에 인덱스 추가
await knex.raw(`ALTER TABLE workers ADD INDEX idx_workers_user_id (user_id)`);
console.log('✅ workers.user_id 추가 및 백필 완료');
}
// ============================================================
// 4~15. 12개 참조 테이블에 user_id 컬럼 추가 + 백필
// ============================================================
// worker_id 컬럼을 가진 테이블들
const tablesWithWorkerId = [
'tbm_team_assignments',
'tbm_transfers',
'daily_work_reports',
'daily_attendance_records',
'worker_vacation_balance',
'vacation_requests',
'vacation_balance_details',
'worker_groups',
'monthly_worker_status',
];
for (const tableName of tablesWithWorkerId) {
const tableExists = await knex.schema.hasTable(tableName);
if (!tableExists) {
console.log(`⏭️ ${tableName} 테이블이 존재하지 않음, 건너뜀`);
continue;
}
const hasUserId = await knex.schema.hasColumn(tableName, 'user_id');
if (!hasUserId) {
await knex.schema.table(tableName, (table) => {
table.integer('user_id').unsigned().defaultTo(null).comment('sso_users.user_id');
});
// 백필: workers 테이블의 user_id 매핑 사용
await knex.raw(`
UPDATE ${tableName} t
INNER JOIN workers w ON t.worker_id = w.worker_id
SET t.user_id = w.user_id
WHERE w.user_id IS NOT NULL
`);
// 인덱스 추가
await knex.raw(`ALTER TABLE ${tableName} ADD INDEX idx_${tableName}_user_id (user_id)`);
console.log(`${tableName}.user_id 추가 및 백필 완료`);
}
}
// DailyIssueReports (대소문자 다른 테이블명)
const hasDIR = await knex.schema.hasTable('DailyIssueReports');
if (hasDIR) {
const hasDIRUserId = await knex.schema.hasColumn('DailyIssueReports', 'user_id');
if (!hasDIRUserId) {
await knex.schema.table('DailyIssueReports', (table) => {
table.integer('user_id').unsigned().defaultTo(null).comment('sso_users.user_id');
});
await knex.raw(`
UPDATE DailyIssueReports t
INNER JOIN workers w ON t.worker_id = w.worker_id
SET t.user_id = w.user_id
WHERE w.user_id IS NOT NULL
`);
console.log('✅ DailyIssueReports.user_id 추가 및 백필 완료');
}
}
// WorkReports (대소문자 다른 테이블명)
const hasWR = await knex.schema.hasTable('WorkReports');
if (hasWR) {
const hasWRUserId = await knex.schema.hasColumn('WorkReports', 'user_id');
if (!hasWRUserId) {
await knex.schema.table('WorkReports', (table) => {
table.integer('user_id').unsigned().defaultTo(null).comment('sso_users.user_id');
});
await knex.raw(`
UPDATE WorkReports t
INNER JOIN workers w ON t.worker_id = w.worker_id
SET t.user_id = w.user_id
WHERE w.user_id IS NOT NULL
`);
console.log('✅ WorkReports.user_id 추가 및 백필 완료');
}
}
// tbm_sessions: leader_id → leader_user_id 추가
const hasLeaderUserId = await knex.schema.hasColumn('tbm_sessions', 'leader_user_id');
if (!hasLeaderUserId) {
await knex.schema.table('tbm_sessions', (table) => {
table.integer('leader_user_id').unsigned().defaultTo(null).comment('조장 sso_users.user_id');
});
await knex.raw(`
UPDATE tbm_sessions t
INNER JOIN workers w ON t.leader_id = w.worker_id
SET t.leader_user_id = w.user_id
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 추가
const hasFromLeaderUserId = await knex.schema.hasColumn('team_handovers', 'from_leader_user_id');
if (!hasFromLeaderUserId) {
await knex.schema.table('team_handovers', (table) => {
table.integer('from_leader_user_id').unsigned().defaultTo(null).comment('인계자 sso_users.user_id');
table.integer('to_leader_user_id').unsigned().defaultTo(null).comment('인수자 sso_users.user_id');
});
await knex.raw(`
UPDATE team_handovers t
INNER JOIN workers w1 ON t.from_leader_id = w1.worker_id
SET t.from_leader_user_id = w1.user_id
WHERE w1.user_id IS NOT NULL
`);
await knex.raw(`
UPDATE team_handovers t
INNER JOIN workers w2 ON t.to_leader_id = w2.worker_id
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) {
// user_id 컬럼 제거 (롤백)
const columnsToRemove = [
['departments', 'is_production'],
['workers', 'user_id'],
['tbm_team_assignments', 'user_id'],
['tbm_transfers', 'user_id'],
['daily_work_reports', 'user_id'],
['daily_attendance_records', 'user_id'],
['worker_vacation_balance', 'user_id'],
['vacation_requests', 'user_id'],
['vacation_balance_details', 'user_id'],
['worker_groups', 'user_id'],
['monthly_worker_status', 'user_id'],
['tbm_sessions', 'leader_user_id'],
['team_handovers', 'from_leader_user_id'],
['team_handovers', 'to_leader_user_id'],
];
for (const [tableName, columnName] of columnsToRemove) {
const tableExists = await knex.schema.hasTable(tableName);
if (!tableExists) continue;
const hasColumn = await knex.schema.hasColumn(tableName, columnName);
if (hasColumn) {
await knex.schema.table(tableName, (table) => {
table.dropColumn(columnName);
});
console.log(`↩️ ${tableName}.${columnName} 제거`);
}
}
// 대소문자 다른 테이블
for (const tableName of ['DailyIssueReports', 'WorkReports']) {
const tableExists = await knex.schema.hasTable(tableName);
if (tableExists) {
const hasColumn = await knex.schema.hasColumn(tableName, 'user_id');
if (hasColumn) {
await knex.schema.table(tableName, (table) => {
table.dropColumn('user_id');
});
console.log(`↩️ ${tableName}.user_id 제거`);
}
}
}
// sso_users.department_id는 유지 (다른 마이그레이션에서 관리)
};