feat: 다수 기능 개선 - 순찰, 출근, 작업분석, 모바일 UI 등
- 순찰/점검 기능 개선 (zone-detail 페이지 추가) - 출근/근태 시스템 개선 (연차 조회, 근무현황) - 작업분석 대분류 그룹화 및 마이그레이션 스크립트 - 모바일 네비게이션 UI 추가 - NAS 배포 도구 및 문서 추가 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,9 @@ router.get('/daily-status', AttendanceController.getDailyAttendanceStatus);
|
||||
// 일일 근태 기록 조회
|
||||
router.get('/daily-records', AttendanceController.getDailyAttendanceRecords);
|
||||
|
||||
// 기간별 근태 기록 조회 (월별 조회용)
|
||||
router.get('/records', AttendanceController.getAttendanceRecordsByRange);
|
||||
|
||||
// 근태 기록 생성/업데이트
|
||||
router.post('/records', AttendanceController.upsertAttendanceRecord);
|
||||
router.put('/records', AttendanceController.upsertAttendanceRecord);
|
||||
|
||||
@@ -147,7 +147,7 @@ router.post('/refresh-token', async (req, res) => {
|
||||
|
||||
// 사용자 정보 조회
|
||||
const [users] = await connection.execute(
|
||||
'SELECT * FROM Users WHERE user_id = ? AND is_active = TRUE',
|
||||
'SELECT * FROM users WHERE user_id = ? AND is_active = TRUE',
|
||||
[decoded.user_id]
|
||||
);
|
||||
|
||||
@@ -229,7 +229,7 @@ router.post('/change-password', verifyToken, async (req, res) => {
|
||||
|
||||
// 현재 사용자의 비밀번호 조회
|
||||
const [users] = await connection.execute(
|
||||
'SELECT password FROM Users WHERE user_id = ?',
|
||||
'SELECT password FROM users WHERE user_id = ?',
|
||||
[userId]
|
||||
);
|
||||
|
||||
@@ -339,7 +339,7 @@ router.post('/admin/change-password', verifyToken, async (req, res) => {
|
||||
|
||||
// 대상 사용자 확인
|
||||
const [users] = await connection.execute(
|
||||
'SELECT username, name FROM Users WHERE user_id = ?',
|
||||
'SELECT username, name FROM users WHERE user_id = ?',
|
||||
[userId]
|
||||
);
|
||||
|
||||
@@ -456,7 +456,7 @@ router.get('/me', verifyToken, async (req, res) => {
|
||||
|
||||
connection = await mysql.createConnection(dbConfig);
|
||||
const [rows] = await connection.execute(
|
||||
'SELECT user_id, username, name, email, access_level, worker_id, last_login_at, created_at FROM Users WHERE user_id = ?',
|
||||
'SELECT user_id, username, name, email, access_level, worker_id, last_login_at, created_at FROM users WHERE user_id = ?',
|
||||
[userId]
|
||||
);
|
||||
|
||||
@@ -522,7 +522,7 @@ router.post('/register', verifyToken, async (req, res) => {
|
||||
|
||||
// 사용자명 중복 체크
|
||||
const [existing] = await connection.execute(
|
||||
'SELECT user_id FROM Users WHERE username = ?',
|
||||
'SELECT user_id FROM users WHERE username = ?',
|
||||
[username]
|
||||
);
|
||||
|
||||
@@ -536,7 +536,7 @@ router.post('/register', verifyToken, async (req, res) => {
|
||||
// 이메일 중복 체크 (이메일이 제공된 경우)
|
||||
if (email) {
|
||||
const [existingEmail] = await connection.execute(
|
||||
'SELECT user_id FROM Users WHERE email = ?',
|
||||
'SELECT user_id FROM users WHERE email = ?',
|
||||
[email]
|
||||
);
|
||||
|
||||
@@ -700,7 +700,7 @@ router.put('/users/:id', verifyToken, async (req, res) => {
|
||||
|
||||
// 사용자 존재 확인
|
||||
const [existing] = await connection.execute(
|
||||
'SELECT user_id, username FROM Users WHERE user_id = ?',
|
||||
'SELECT user_id, username FROM users WHERE user_id = ?',
|
||||
[userId]
|
||||
);
|
||||
|
||||
@@ -724,7 +724,7 @@ router.put('/users/:id', verifyToken, async (req, res) => {
|
||||
// 이메일 중복 체크
|
||||
if (email) {
|
||||
const [emailCheck] = await connection.execute(
|
||||
'SELECT user_id FROM Users WHERE email = ? AND user_id != ?',
|
||||
'SELECT user_id FROM users WHERE email = ? AND user_id != ?',
|
||||
[email, userId]
|
||||
);
|
||||
|
||||
@@ -794,7 +794,7 @@ router.put('/users/:id', verifyToken, async (req, res) => {
|
||||
|
||||
// 업데이트된 사용자 정보 조회
|
||||
const [updated] = await connection.execute(
|
||||
'SELECT user_id, username, name, email, access_level, worker_id, is_active FROM Users WHERE user_id = ?',
|
||||
'SELECT user_id, username, name, email, access_level, worker_id, is_active FROM users WHERE user_id = ?',
|
||||
[userId]
|
||||
);
|
||||
|
||||
|
||||
@@ -25,4 +25,99 @@ router.get('/detail', (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
// 임시 마이그레이션 엔드포인트 - TBM work_type_id 수정
|
||||
// 실행 후 이 코드를 삭제하세요!
|
||||
router.post('/migrate-work-type-id', async (req, res) => {
|
||||
try {
|
||||
const { getDb } = require('../dbPool');
|
||||
const db = await getDb();
|
||||
|
||||
console.log('🔄 TBM 기반 작업보고서 work_type_id 수정 시작...');
|
||||
|
||||
// 1. 수정 대상 확인
|
||||
const [checkResult] = await db.query(`
|
||||
SELECT
|
||||
dwr.id,
|
||||
dwr.work_type_id as current_work_type_id,
|
||||
ta.task_id as correct_task_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}개 레코드`);
|
||||
|
||||
if (checkResult.length === 0) {
|
||||
return res.json({
|
||||
success: true,
|
||||
message: '수정할 데이터가 없습니다.',
|
||||
data: { affected_rows: 0 }
|
||||
});
|
||||
}
|
||||
|
||||
// 수정 전 샘플 로깅
|
||||
console.log('수정 전 샘플:', checkResult.slice(0, 5));
|
||||
|
||||
// 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(`✅ 업데이트 완료: ${updateResult.affectedRows}개 레코드 수정됨`);
|
||||
|
||||
// 3. 수정 후 확인
|
||||
const [samples] = await db.query(`
|
||||
SELECT
|
||||
dwr.id,
|
||||
dwr.work_type_id,
|
||||
t.task_name,
|
||||
wt.name as work_type_name,
|
||||
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
|
||||
LEFT JOIN tasks t ON dwr.work_type_id = t.task_id
|
||||
LEFT JOIN work_types wt ON t.work_type_id = wt.id
|
||||
LEFT JOIN workers w ON dwr.worker_id = w.worker_id
|
||||
WHERE dwr.tbm_assignment_id IS NOT NULL
|
||||
ORDER BY dwr.report_date DESC
|
||||
LIMIT 10
|
||||
`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `${updateResult.affectedRows}개 레코드가 수정되었습니다.`,
|
||||
data: {
|
||||
affected_rows: updateResult.affectedRows,
|
||||
before_count: checkResult.length,
|
||||
samples: samples.map(s => ({
|
||||
id: s.id,
|
||||
worker: s.worker_name,
|
||||
date: s.report_date,
|
||||
task: s.task_name,
|
||||
work_type: s.work_type_name
|
||||
}))
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('마이그레이션 실패:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: '마이그레이션 실패: ' + error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -3,8 +3,35 @@
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const multer = require('multer');
|
||||
const path = require('path');
|
||||
const patrolController = require('../controllers/patrolController');
|
||||
|
||||
// Multer 설정 - 구역 현황 사진 업로드
|
||||
const storage = multer.diskStorage({
|
||||
destination: (req, file, cb) => {
|
||||
cb(null, path.join(__dirname, '../uploads'));
|
||||
},
|
||||
filename: (req, file, cb) => {
|
||||
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
|
||||
const ext = path.extname(file.originalname).toLowerCase();
|
||||
cb(null, `zone-item-${uniqueSuffix}${ext}`);
|
||||
}
|
||||
});
|
||||
|
||||
const upload = multer({
|
||||
storage,
|
||||
limits: { fileSize: 10 * 1024 * 1024 }, // 10MB
|
||||
fileFilter: (req, file, cb) => {
|
||||
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
|
||||
if (allowedTypes.includes(file.mimetype)) {
|
||||
cb(null, true);
|
||||
} else {
|
||||
cb(new Error('이미지 파일만 업로드 가능합니다.'), false);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// ==================== 순회점검 세션 ====================
|
||||
|
||||
// 세션 목록 조회
|
||||
@@ -70,4 +97,30 @@ router.get('/item-types', patrolController.getItemTypes);
|
||||
// 오늘 순회점검 현황
|
||||
router.get('/today-status', patrolController.getTodayStatus);
|
||||
|
||||
// ==================== 작업장 상세 정보 ====================
|
||||
|
||||
// 작업장 상세 정보 조회 (시설물, 안전신고, 부적합, 출입, TBM 통합)
|
||||
// GET /patrol/workplaces/:workplaceId/detail?date=2026-02-05
|
||||
router.get('/workplaces/:workplaceId/detail', patrolController.getWorkplaceDetail);
|
||||
|
||||
// ==================== 구역 내 등록 물품/시설물 ====================
|
||||
|
||||
// 구역 내 등록된 물품/시설물 목록 조회
|
||||
router.get('/workplaces/:workplaceId/zone-items', patrolController.getZoneItems);
|
||||
|
||||
// 구역 내 물품/시설물 등록
|
||||
router.post('/workplaces/:workplaceId/zone-items', patrolController.createZoneItem);
|
||||
|
||||
// 구역 내 물품/시설물 수정
|
||||
router.put('/zone-items/:itemId', patrolController.updateZoneItem);
|
||||
|
||||
// 구역 내 물품/시설물 삭제
|
||||
router.delete('/zone-items/:itemId', patrolController.deleteZoneItem);
|
||||
|
||||
// 구역 현황 사진 업로드
|
||||
router.post('/zone-items/photos', upload.single('photo'), patrolController.uploadZoneItemPhoto);
|
||||
|
||||
// 구역 현황 이력 조회
|
||||
router.get('/zone-items/:itemId/history', patrolController.getZoneItemHistory);
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -210,6 +210,98 @@ router.get('/logs/password-changes', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/system/migrations/fix-work-type-id
|
||||
* TBM 기반 작업보고서의 work_type_id를 task_id로 수정
|
||||
*/
|
||||
router.post('/migrations/fix-work-type-id', async (req, res) => {
|
||||
try {
|
||||
const { getDb } = require('../dbPool');
|
||||
const db = await getDb();
|
||||
|
||||
console.log('🔄 TBM 기반 작업보고서 work_type_id 수정 시작...');
|
||||
|
||||
// 1. 수정 대상 확인
|
||||
const [checkResult] = await db.query(`
|
||||
SELECT
|
||||
dwr.id,
|
||||
dwr.work_type_id as current_work_type_id,
|
||||
ta.task_id as correct_task_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
|
||||
`);
|
||||
|
||||
if (checkResult.length === 0) {
|
||||
return res.json({
|
||||
success: true,
|
||||
message: '수정할 데이터가 없습니다.',
|
||||
data: { affected_rows: 0, samples: [] }
|
||||
});
|
||||
}
|
||||
|
||||
// 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(`✅ 업데이트 완료: ${updateResult.affectedRows}개 레코드 수정됨`);
|
||||
|
||||
// 3. 수정된 샘플 조회
|
||||
const [samples] = await db.query(`
|
||||
SELECT
|
||||
dwr.id,
|
||||
dwr.work_type_id,
|
||||
t.task_name,
|
||||
wt.name as work_type_name,
|
||||
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
|
||||
LEFT JOIN tasks t ON dwr.work_type_id = t.task_id
|
||||
LEFT JOIN work_types wt ON t.work_type_id = wt.id
|
||||
LEFT JOIN workers w ON dwr.worker_id = w.worker_id
|
||||
WHERE dwr.tbm_assignment_id IS NOT NULL
|
||||
ORDER BY dwr.report_date DESC
|
||||
LIMIT 10
|
||||
`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `${updateResult.affectedRows}개 레코드가 수정되었습니다.`,
|
||||
data: {
|
||||
affected_rows: updateResult.affectedRows,
|
||||
before_count: checkResult.length,
|
||||
samples: samples.map(s => ({
|
||||
id: s.id,
|
||||
worker: s.worker_name,
|
||||
date: s.report_date,
|
||||
task: s.task_name,
|
||||
work_type: s.work_type_name
|
||||
}))
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('마이그레이션 실패:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: '마이그레이션 실패: ' + error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/system/logs/activity
|
||||
* 활동 로그 조회 (activity_logs 테이블이 있는 경우)
|
||||
|
||||
@@ -22,6 +22,9 @@ router.post('/auto-calculate', vacationBalanceController.autoCalculateAndCreate)
|
||||
// 휴가 잔액 생성 (관리자만)
|
||||
router.post('/', vacationBalanceController.createBalance);
|
||||
|
||||
// 휴가 잔액 일괄 저장 (upsert)
|
||||
router.post('/bulk-upsert', vacationBalanceController.bulkUpsert);
|
||||
|
||||
// 휴가 잔액 수정 (관리자만)
|
||||
router.put('/:id', vacationBalanceController.updateBalance);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user