feat(sprint-002): 대리입력 + 일별 현황 대시보드 (Section A+B)
Section A (Backend): - POST /api/proxy-input: TBM 세션+팀배정+작업보고서 일괄 생성 (트랜잭션) - GET /api/proxy-input/daily-status: 일별 TBM/보고서 입력 현황 - GET /api/proxy-input/daily-status/detail: 작업자별 상세 - tbm_sessions에 is_proxy_input, proxy_input_by 컬럼 추가 - system1/system2/tkuser requireMinLevel → shared requirePage 전환 - permissionModel에 factory_proxy_input, factory_daily_status 키 등록 Section B (Frontend): - daily-status.html: 날짜 네비 + 요약 카드 + 필터 탭 + 작업자 리스트 + 바텀시트 - proxy-input.html: 미입력자 카드 + 확장 폼 + 일괄 설정 + 저장 - tkfb-core.js NAV_MENU에 입력 현황/대리입력 추가 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
187
system1-factory/api/controllers/proxyInputController.js
Normal file
187
system1-factory/api/controllers/proxyInputController.js
Normal file
@@ -0,0 +1,187 @@
|
||||
/**
|
||||
* 대리입력 + 일별 현황 컨트롤러
|
||||
*/
|
||||
const ProxyInputModel = require('../models/proxyInputModel');
|
||||
const { getDb } = require('../dbPool');
|
||||
const logger = require('../../shared/utils/logger');
|
||||
|
||||
const ProxyInputController = {
|
||||
/**
|
||||
* POST /api/proxy-input — 대리입력 (단일 트랜잭션)
|
||||
*/
|
||||
proxyInput: async (req, res) => {
|
||||
const { session_date, leader_id, entries, safety_notes, work_location } = req.body;
|
||||
const userId = req.user.user_id || req.user.id;
|
||||
|
||||
// 유효성 검사
|
||||
if (!session_date) {
|
||||
return res.status(400).json({ success: false, message: '날짜는 필수입니다.' });
|
||||
}
|
||||
if (!entries || !Array.isArray(entries) || entries.length === 0) {
|
||||
return res.status(400).json({ success: false, message: '작업자 정보는 최소 1명 필요합니다.' });
|
||||
}
|
||||
if (entries.length > 30) {
|
||||
return res.status(400).json({ success: false, message: '한 번에 30명까지 입력 가능합니다.' });
|
||||
}
|
||||
|
||||
// 날짜 유효성 (과거 30일 ~ 오늘)
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
const inputDate = new Date(session_date);
|
||||
const diffDays = Math.floor((today - inputDate) / (1000 * 60 * 60 * 24));
|
||||
if (diffDays < 0) {
|
||||
return res.status(400).json({ success: false, message: '미래 날짜는 입력할 수 없습니다.' });
|
||||
}
|
||||
if (diffDays > 30) {
|
||||
return res.status(400).json({ success: false, message: '30일 이내 날짜만 입력 가능합니다.' });
|
||||
}
|
||||
|
||||
// entries 필수 필드 검사
|
||||
for (const entry of entries) {
|
||||
if (!entry.user_id || !entry.project_id || !entry.work_type_id || !entry.work_hours) {
|
||||
return res.status(400).json({ success: false, message: '각 작업자의 user_id, project_id, work_type_id, work_hours는 필수입니다.' });
|
||||
}
|
||||
if (entry.work_hours <= 0 || entry.work_hours > 24) {
|
||||
return res.status(400).json({ success: false, message: '근무 시간은 0 초과 24 이하여야 합니다.' });
|
||||
}
|
||||
}
|
||||
|
||||
const db = await getDb();
|
||||
const conn = await db.getConnection();
|
||||
|
||||
try {
|
||||
await conn.beginTransaction();
|
||||
|
||||
const userIds = entries.map(e => e.user_id);
|
||||
|
||||
// 1. 중복 체크
|
||||
const duplicates = await ProxyInputModel.checkDuplicateAssignments(conn, session_date, userIds);
|
||||
if (duplicates.length > 0) {
|
||||
await conn.rollback();
|
||||
return res.status(409).json({
|
||||
success: false,
|
||||
message: `다음 작업자가 이미 해당 날짜에 TBM 배정되어 있습니다: ${duplicates.map(d => d.worker_name).join(', ')}`,
|
||||
data: { duplicate_workers: duplicates }
|
||||
});
|
||||
}
|
||||
|
||||
// 2. 작업자 존재 체크
|
||||
const validWorkerIds = await ProxyInputModel.validateWorkers(conn, userIds);
|
||||
const invalidIds = userIds.filter(id => !validWorkerIds.includes(id));
|
||||
if (invalidIds.length > 0) {
|
||||
await conn.rollback();
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: `존재하지 않거나 비활성 작업자: ${invalidIds.join(', ')}`
|
||||
});
|
||||
}
|
||||
|
||||
// 3. TBM 세션 생성
|
||||
const sessionResult = await ProxyInputModel.createProxySession(conn, {
|
||||
session_date,
|
||||
leader_id: leader_id || userId,
|
||||
proxy_input_by: userId,
|
||||
created_by: userId,
|
||||
safety_notes: safety_notes || '',
|
||||
work_location: work_location || ''
|
||||
});
|
||||
const sessionId = sessionResult.insertId;
|
||||
|
||||
// 4. 각 entry 처리
|
||||
const createdWorkers = [];
|
||||
for (const entry of entries) {
|
||||
// 팀 배정
|
||||
const assignResult = await ProxyInputModel.createTeamAssignment(conn, {
|
||||
session_id: sessionId,
|
||||
user_id: entry.user_id,
|
||||
project_id: entry.project_id,
|
||||
work_type_id: entry.work_type_id,
|
||||
task_id: entry.task_id || null,
|
||||
workplace_id: entry.workplace_id || null,
|
||||
work_hours: entry.work_hours
|
||||
});
|
||||
const assignmentId = assignResult.insertId;
|
||||
|
||||
// 작업보고서
|
||||
const reportResult = await ProxyInputModel.createWorkReport(conn, {
|
||||
report_date: session_date,
|
||||
user_id: entry.user_id,
|
||||
project_id: entry.project_id,
|
||||
work_type_id: entry.work_type_id,
|
||||
task_id: entry.task_id || null,
|
||||
work_status_id: entry.work_status_id || 1,
|
||||
work_hours: entry.work_hours,
|
||||
start_time: entry.start_time || null,
|
||||
end_time: entry.end_time || null,
|
||||
note: entry.note || '',
|
||||
tbm_session_id: sessionId,
|
||||
tbm_assignment_id: assignmentId,
|
||||
created_by: userId
|
||||
});
|
||||
|
||||
createdWorkers.push({
|
||||
user_id: entry.user_id,
|
||||
report_id: reportResult.insertId
|
||||
});
|
||||
}
|
||||
|
||||
await conn.commit();
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: `${entries.length}명의 대리입력이 완료되었습니다.`,
|
||||
data: {
|
||||
session_id: sessionId,
|
||||
is_proxy_input: true,
|
||||
created_reports: entries.length,
|
||||
workers: createdWorkers
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
try { await conn.rollback(); } catch (e) {}
|
||||
logger.error('대리입력 오류:', err);
|
||||
res.status(500).json({ success: false, message: '대리입력 처리 중 오류가 발생했습니다.', error: err.message });
|
||||
} finally {
|
||||
conn.release();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* GET /api/proxy-input/daily-status — 일별 현황
|
||||
*/
|
||||
getDailyStatus: async (req, res) => {
|
||||
try {
|
||||
const { date } = req.query;
|
||||
if (!date) {
|
||||
return res.status(400).json({ success: false, message: '날짜(date) 파라미터는 필수입니다.' });
|
||||
}
|
||||
const data = await ProxyInputModel.getDailyStatus(date);
|
||||
res.json({ success: true, data });
|
||||
} catch (err) {
|
||||
logger.error('일별 현황 조회 오류:', err);
|
||||
res.status(500).json({ success: false, message: '조회 중 오류가 발생했습니다.', error: err.message });
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* GET /api/proxy-input/daily-status/detail — 작업자별 상세
|
||||
*/
|
||||
getDailyStatusDetail: async (req, res) => {
|
||||
try {
|
||||
const { date, user_id } = req.query;
|
||||
if (!date || !user_id) {
|
||||
return res.status(400).json({ success: false, message: 'date와 user_id 파라미터는 필수입니다.' });
|
||||
}
|
||||
const data = await ProxyInputModel.getDailyStatusDetail(date, parseInt(user_id));
|
||||
if (!data.worker) {
|
||||
return res.status(404).json({ success: false, message: '작업자를 찾을 수 없습니다.' });
|
||||
}
|
||||
res.json({ success: true, data });
|
||||
} catch (err) {
|
||||
logger.error('일별 상세 조회 오류:', err);
|
||||
res.status(500).json({ success: false, message: '조회 중 오류가 발생했습니다.', error: err.message });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = ProxyInputController;
|
||||
Reference in New Issue
Block a user