feat: 다수 기능 개선 - 순찰, 출근, 작업분석, 모바일 UI 등

- 순찰/점검 기능 개선 (zone-detail 페이지 추가)
- 출근/근태 시스템 개선 (연차 조회, 근무현황)
- 작업분석 대분류 그룹화 및 마이그레이션 스크립트
- 모바일 네비게이션 UI 추가
- NAS 배포 도구 및 문서 추가

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Hyungi Ahn
2026-02-09 14:41:01 +09:00
parent 1548253f56
commit 2b1c7bfb88
633 changed files with 361224 additions and 1090 deletions

View File

@@ -0,0 +1,105 @@
/**
* 마이그레이션: 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,56 @@
-- ============================================
-- error_types → issue_report_items 마이그레이션
-- 실행 전 반드시 백업하세요!
-- ============================================
-- STEP 1: 현재 상태 확인
-- ============================================
SELECT 'Before Migration' as status;
SELECT error_type_id, COUNT(*) as cnt FROM daily_work_reports WHERE error_type_id IS NOT NULL GROUP BY error_type_id;
-- STEP 2: 매핑 업데이트 실행
-- ============================================
-- 주의: 순서가 중요! (충돌 방지를 위해 큰 숫자부터)
-- 6 (검사불량) → 14 (치수 검사 누락)
UPDATE daily_work_reports SET error_type_id = 14 WHERE error_type_id = 6;
-- 5 (설비고장) → 38 (기계 고장)
UPDATE daily_work_reports SET error_type_id = 38 WHERE error_type_id = 5;
-- 4 (작업불량) → 43 (NDE 불합격)
UPDATE daily_work_reports SET error_type_id = 43 WHERE error_type_id = 4;
-- 3 (입고지연) → 1 (배관 자재 미입고) - 이미 1이므로 충돌 가능, 임시값 사용
UPDATE daily_work_reports SET error_type_id = 99991 WHERE error_type_id = 3;
-- 2 (외주작업 불량) → 10 (외관 불량)
UPDATE daily_work_reports SET error_type_id = 10 WHERE error_type_id = 2;
-- 1 (설계미스) → 6 (도면 치수 오류) - 6은 이미 업데이트됨
UPDATE daily_work_reports SET error_type_id = 6 WHERE error_type_id = 1;
-- 임시값 복원: 99991 → 1
UPDATE daily_work_reports SET error_type_id = 1 WHERE error_type_id = 99991;
-- STEP 3: 마이그레이션 결과 확인
-- ============================================
SELECT 'After Migration' as status;
SELECT
dwr.error_type_id,
iri.item_name,
irc.category_name,
COUNT(*) as cnt
FROM daily_work_reports dwr
LEFT JOIN issue_report_items iri ON dwr.error_type_id = iri.item_id
LEFT JOIN issue_report_categories irc ON iri.category_id = irc.category_id
WHERE dwr.error_type_id IS NOT NULL
GROUP BY dwr.error_type_id, iri.item_name, irc.category_name;
-- STEP 4: error_types 테이블 삭제 (선택사항)
-- ============================================
-- 마이그레이션 확인 후 주석 해제하여 실행
-- DROP TABLE IF EXISTS error_types;

View File

@@ -0,0 +1,112 @@
/**
* 마이그레이션: error_types에서 issue_report_items로 전환
*
* 기존 daily_work_reports.error_type_id가 error_types.id를 참조하던 것을
* issue_report_items.item_id를 참조하도록 변경
*
* 기존 error_types 데이터:
* id=1: 설계미스
* id=2: 외주작업 불량
* id=3: 입고지연
*
* 새 issue_report_categories 데이터:
* category_id=1: 자재누락 (nonconformity)
* category_id=2: 설계미스 (nonconformity)
* category_id=3: 입고불량 (nonconformity)
*
* 매핑 전략:
* - error_types.id=1 (설계미스) → issue_report_items에서 '설계미스' 카테고리의 첫 번째 항목
* - error_types.id=2 (외주작업 불량) → issue_report_items에서 '입고불량' 카테고리의 '외관 불량' 항목
* - error_types.id=3 (입고지연) → issue_report_items에서 '자재누락' 카테고리의 첫 번째 항목
*/
exports.up = async function(knex) {
console.log('=== error_type_id 마이그레이션 시작 ===');
// 1. 기존 error_types 데이터와 새 issue_report_items 매핑 테이블 조회
const [categories] = await knex.raw(`
SELECT category_id, category_name
FROM issue_report_categories
WHERE category_type = 'nonconformity'
`);
console.log('부적합 카테고리:', categories);
const [items] = await knex.raw(`
SELECT iri.item_id, iri.item_name, iri.category_id, irc.category_name
FROM issue_report_items iri
JOIN issue_report_categories irc ON iri.category_id = irc.category_id
WHERE irc.category_type = 'nonconformity'
ORDER BY iri.category_id, iri.display_order
`);
console.log('부적합 항목:', items);
// 2. 매핑 정의 (기존 error_type_id → 새 issue_report_items.item_id)
// 설계미스 카테고리 찾기
const designMissCategory = categories.find(c => c.category_name === '설계미스');
const incomingDefectCategory = categories.find(c => c.category_name === '입고불량');
const materialShortageCategory = categories.find(c => c.category_name === '자재누락');
// 각 카테고리의 첫 번째 항목 찾기
const designMissItem = items.find(i => i.category_id === designMissCategory?.category_id);
const incomingDefectItem = items.find(i => i.category_id === incomingDefectCategory?.category_id);
const materialShortageItem = items.find(i => i.category_id === materialShortageCategory?.category_id);
console.log('매핑 결과:');
console.log(' - 설계미스(1) → item_id:', designMissItem?.item_id);
console.log(' - 외주작업불량(2) → item_id:', incomingDefectItem?.item_id);
console.log(' - 입고지연(3) → item_id:', materialShortageItem?.item_id);
// 3. 기존 데이터 업데이트
if (designMissItem) {
const [result1] = await knex.raw(`
UPDATE daily_work_reports
SET error_type_id = ?
WHERE error_type_id = 1
`, [designMissItem.item_id]);
console.log('설계미스(1) 업데이트:', result1.affectedRows, '건');
}
if (incomingDefectItem) {
const [result2] = await knex.raw(`
UPDATE daily_work_reports
SET error_type_id = ?
WHERE error_type_id = 2
`, [incomingDefectItem.item_id]);
console.log('외주작업불량(2) 업데이트:', result2.affectedRows, '건');
}
if (materialShortageItem) {
const [result3] = await knex.raw(`
UPDATE daily_work_reports
SET error_type_id = ?
WHERE error_type_id = 3
`, [materialShortageItem.item_id]);
console.log('입고지연(3) 업데이트:', result3.affectedRows, '건');
}
// 4. 매핑 안된 나머지 데이터 확인 (4 이상의 error_type_id)
const [unmapped] = await knex.raw(`
SELECT DISTINCT error_type_id, COUNT(*) as cnt
FROM daily_work_reports
WHERE error_type_id IS NOT NULL
AND error_type_id NOT IN (?, ?, ?)
GROUP BY error_type_id
`, [
designMissItem?.item_id || 0,
incomingDefectItem?.item_id || 0,
materialShortageItem?.item_id || 0
]);
if (unmapped.length > 0) {
console.log('⚠️ 매핑되지 않은 error_type_id 발견:', unmapped);
console.log(' 이 데이터는 수동으로 확인 필요');
}
console.log('=== error_type_id 마이그레이션 완료 ===');
};
exports.down = async function(knex) {
// 롤백은 복잡하므로 로그만 출력
console.log('⚠️ 이 마이그레이션은 자동 롤백을 지원하지 않습니다.');
console.log(' 데이터 복구가 필요한 경우 백업에서 복원해주세요.');
};

View File

@@ -0,0 +1,73 @@
-- ============================================
-- error_types → issue_report_items 마이그레이션
-- ============================================
-- STEP 1: 현재 데이터 확인
-- ============================================
-- 기존 error_types 확인
SELECT * FROM error_types;
-- 새 issue_report_categories 확인 (부적합만)
SELECT * FROM issue_report_categories WHERE category_type = 'nonconformity';
-- 새 issue_report_items 확인 (부적합만)
SELECT
iri.item_id,
iri.item_name,
iri.category_id,
irc.category_name
FROM issue_report_items iri
JOIN issue_report_categories irc ON iri.category_id = irc.category_id
WHERE irc.category_type = 'nonconformity'
ORDER BY irc.display_order, iri.display_order;
-- 현재 daily_work_reports에서 사용 중인 error_type_id 확인
SELECT
error_type_id,
COUNT(*) as cnt,
et.name as old_error_name
FROM daily_work_reports dwr
LEFT JOIN error_types et ON dwr.error_type_id = et.id
WHERE error_type_id IS NOT NULL
GROUP BY error_type_id
ORDER BY error_type_id;
-- STEP 2: 매핑 업데이트 (실제 item_id 확인 후 수정 필요!)
-- ============================================
-- 먼저 위 쿼리로 실제 item_id 값을 확인하세요!
-- 아래는 예시입니다. 실제 값으로 수정해서 사용하세요.
-- 예시: 설계미스(error_type_id=1) → 설계미스 카테고리의 '도면 치수 오류' 항목
-- UPDATE daily_work_reports SET error_type_id = 6 WHERE error_type_id = 1;
-- 예시: 외주작업 불량(error_type_id=2) → 입고불량 카테고리의 '외관 불량' 항목
-- UPDATE daily_work_reports SET error_type_id = 10 WHERE error_type_id = 2;
-- 예시: 입고지연(error_type_id=3) → 자재누락 카테고리의 '배관 자재 미입고' 항목
-- UPDATE daily_work_reports SET error_type_id = 1 WHERE error_type_id = 3;
-- STEP 3: 매핑 검증
-- ============================================
-- 업데이트 후 확인
SELECT
dwr.error_type_id,
iri.item_name,
irc.category_name,
COUNT(*) as cnt
FROM daily_work_reports dwr
LEFT JOIN issue_report_items iri ON dwr.error_type_id = iri.item_id
LEFT JOIN issue_report_categories irc ON iri.category_id = irc.category_id
WHERE dwr.error_type_id IS NOT NULL
GROUP BY dwr.error_type_id, iri.item_name, irc.category_name;
-- STEP 4: error_types 테이블 삭제 (매핑 완료 후)
-- ============================================
-- 주의: 반드시 STEP 2, 3 완료 후 실행!
-- DROP TABLE IF EXISTS error_types;