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

@@ -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는 유지 (다른 마이그레이션에서 관리)
};