feat(tksafety): 체크리스트 작업별 항목에 tkuser 작업(task) 참조 연동
- getAllChecks: tasks/work_types/weather_conditions JOIN + 프론트엔드 필드명 alias - createCheck/updateCheck: item_type→check_type 등 프론트-DB 필드 매핑 - 모달에 작업(task) 드롭다운 추가, 공정 선택 시 동적 로드 - renderWorktypeItems: work_type → task 2단 그룹핑 - openEditItem: async/await로 task 목록 로드 후 값 설정 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -23,7 +23,7 @@ exports.getCheckById = async (req, res) => {
|
||||
|
||||
exports.createCheck = async (req, res) => {
|
||||
try {
|
||||
if (!req.body.check_item) return res.status(400).json({ success: false, error: 'check_item은 필수입니다' });
|
||||
if (!req.body.item_content && !req.body.check_item) return res.status(400).json({ success: false, error: '점검 항목 내용은 필수입니다' });
|
||||
const checkId = await checklistModel.createCheck(req.body);
|
||||
res.status(201).json({ success: true, message: '항목이 추가되었습니다', data: { check_id: checkId } });
|
||||
} catch (err) {
|
||||
|
||||
@@ -3,7 +3,19 @@ const { getPool } = require('../middleware/auth');
|
||||
// Get all safety checks
|
||||
async function getAllChecks() {
|
||||
const db = getPool();
|
||||
const [rows] = await db.query('SELECT * FROM tbm_safety_checks ORDER BY check_type, check_category, display_order, check_id');
|
||||
const [rows] = await db.query(`
|
||||
SELECT c.check_id AS item_id, c.check_type AS item_type,
|
||||
c.check_item AS item_content, c.check_category AS category,
|
||||
c.display_order, c.is_active, c.is_required, c.description,
|
||||
c.weather_condition, wc.condition_name AS weather_condition_name,
|
||||
wc.condition_code AS weather_condition_id,
|
||||
c.task_id, t.task_name, t.work_type_id, wt.name AS work_type_name
|
||||
FROM tbm_safety_checks c
|
||||
LEFT JOIN tasks t ON c.task_id = t.task_id
|
||||
LEFT JOIN work_types wt ON t.work_type_id = wt.id
|
||||
LEFT JOIN weather_conditions wc ON c.weather_condition = wc.condition_code
|
||||
ORDER BY c.check_type, c.check_category, c.display_order, c.check_id
|
||||
`);
|
||||
return rows;
|
||||
}
|
||||
|
||||
@@ -14,24 +26,32 @@ async function getCheckById(checkId) {
|
||||
return rows[0];
|
||||
}
|
||||
|
||||
// Create check
|
||||
// Create check (frontend field names → DB column mapping)
|
||||
async function createCheck(data) {
|
||||
const db = getPool();
|
||||
const { check_type, check_category, check_item, description, is_required, display_order, weather_condition, task_id } = data;
|
||||
const check_type = data.item_type || data.check_type;
|
||||
const check_category = data.category || data.check_category || null;
|
||||
const check_item = data.item_content || data.check_item;
|
||||
const weather_condition = data.weather_condition_id || data.weather_condition || null;
|
||||
const { description, is_required, display_order, task_id } = data;
|
||||
const [result] = await db.query(
|
||||
'INSERT INTO tbm_safety_checks (check_type, check_category, check_item, description, is_required, display_order, weather_condition, task_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
[check_type, check_category || null, check_item, description || null, is_required ? 1 : 0, display_order || 0, weather_condition || null, task_id || null]
|
||||
[check_type, check_category, check_item, description || null, is_required ? 1 : 0, display_order || 0, weather_condition, task_id || null]
|
||||
);
|
||||
return result.insertId;
|
||||
}
|
||||
|
||||
// Update check
|
||||
// Update check (frontend field names → DB column mapping)
|
||||
async function updateCheck(checkId, data) {
|
||||
const db = getPool();
|
||||
const { check_type, check_category, check_item, description, is_required, display_order, weather_condition, task_id } = data;
|
||||
const check_type = data.item_type || data.check_type;
|
||||
const check_category = data.category || data.check_category || null;
|
||||
const check_item = data.item_content || data.check_item;
|
||||
const weather_condition = data.weather_condition_id || data.weather_condition || null;
|
||||
const { description, is_required, display_order, task_id } = data;
|
||||
const [result] = await db.query(
|
||||
'UPDATE tbm_safety_checks SET check_type = ?, check_category = ?, check_item = ?, description = ?, is_required = ?, display_order = ?, weather_condition = ?, task_id = ? WHERE check_id = ?',
|
||||
[check_type, check_category || null, check_item, description || null, is_required ? 1 : 0, display_order || 0, weather_condition || null, task_id || null, checkId]
|
||||
[check_type, check_category, check_item, description || null, is_required ? 1 : 0, display_order || 0, weather_condition, task_id || null, checkId]
|
||||
);
|
||||
return result;
|
||||
}
|
||||
@@ -53,14 +73,14 @@ async function getWeatherConditions() {
|
||||
// Get work types
|
||||
async function getWorkTypes() {
|
||||
const db = getPool();
|
||||
const [rows] = await db.query('SELECT * FROM work_types ORDER BY name');
|
||||
const [rows] = await db.query('SELECT id AS work_type_id, name AS work_type_name, category, description FROM work_types ORDER BY name');
|
||||
return rows;
|
||||
}
|
||||
|
||||
// Get tasks by work type
|
||||
async function getTasksByWorkType(workTypeId) {
|
||||
const db = getPool();
|
||||
const [rows] = await db.query('SELECT * FROM tasks WHERE work_type_id = ? ORDER BY task_name', [workTypeId]);
|
||||
const [rows] = await db.query('SELECT task_id, task_name, work_type_id, description, is_active FROM tasks WHERE work_type_id = ? AND is_active = TRUE ORDER BY task_name', [workTypeId]);
|
||||
return rows;
|
||||
}
|
||||
|
||||
|
||||
@@ -122,11 +122,18 @@
|
||||
</div>
|
||||
<!-- 작업유형 (work_type only) -->
|
||||
<div id="workTypeField" class="hidden">
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">작업 유형 <span class="text-red-400">*</span></label>
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">공정 <span class="text-red-400">*</span></label>
|
||||
<select id="itemWorkType" class="input-field w-full px-3 py-2 rounded-lg text-sm">
|
||||
<option value="">선택</option>
|
||||
</select>
|
||||
</div>
|
||||
<!-- 작업 선택 (work_type 선택 시 표시) -->
|
||||
<div id="taskField" class="hidden">
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">작업 <span class="text-red-400">*</span></label>
|
||||
<select id="itemTask" class="input-field w-full px-3 py-2 rounded-lg text-sm">
|
||||
<option value="">공정을 먼저 선택하세요</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">카테고리</label>
|
||||
<input type="text" id="itemCategory" class="input-field w-full px-3 py-2 rounded-lg text-sm" placeholder="예: 개인보호구, 작업환경">
|
||||
@@ -155,7 +162,7 @@
|
||||
</div>
|
||||
|
||||
<script src="/static/js/tksafety-core.js?v=3"></script>
|
||||
<script src="/static/js/tksafety-checklist.js"></script>
|
||||
<script src="/static/js/tksafety-checklist.js?v=2"></script>
|
||||
<script>initChecklistPage();</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -53,6 +53,32 @@ async function loadLookupData() {
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== Load tasks by work type ===== */
|
||||
async function loadTasksByWorkType(workTypeId) {
|
||||
const taskSelect = document.getElementById('itemTask');
|
||||
if (!workTypeId) {
|
||||
taskSelect.innerHTML = '<option value="">공정을 먼저 선택하세요</option>';
|
||||
document.getElementById('taskField').classList.add('hidden');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const res = await api('/checklist/tasks/' + workTypeId);
|
||||
const tasks = res.data || [];
|
||||
taskSelect.innerHTML = '<option value="">작업 선택</option>' +
|
||||
tasks.map(t => `<option value="${t.task_id}">${escapeHtml(t.task_name)}</option>`).join('');
|
||||
document.getElementById('taskField').classList.remove('hidden');
|
||||
} catch (e) {
|
||||
console.error('Task load error:', e);
|
||||
taskSelect.innerHTML = '<option value="">로드 실패</option>';
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== Work type change handler ===== */
|
||||
function onWorkTypeChange() {
|
||||
const workTypeId = document.getElementById('itemWorkType').value;
|
||||
loadTasksByWorkType(workTypeId);
|
||||
}
|
||||
|
||||
/* ===== Render basic items ===== */
|
||||
function renderBasicItems() {
|
||||
const items = checklistItems.filter(i => i.item_type === 'basic');
|
||||
@@ -109,7 +135,7 @@ function renderWeatherItems() {
|
||||
`).join('');
|
||||
}
|
||||
|
||||
/* ===== Render worktype items ===== */
|
||||
/* ===== Render worktype items (2-level: work_type → task) ===== */
|
||||
function renderWorktypeItems() {
|
||||
const items = checklistItems.filter(i => i.item_type === 'work_type');
|
||||
const container = document.getElementById('worktypeItemsList');
|
||||
@@ -118,22 +144,31 @@ function renderWorktypeItems() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Group by work type
|
||||
const groups = {};
|
||||
// 2-level grouping: work_type → task
|
||||
const wtGroups = {};
|
||||
items.forEach(i => {
|
||||
const wt = i.work_type_name || '미지정';
|
||||
if (!groups[wt]) groups[wt] = [];
|
||||
groups[wt].push(i);
|
||||
if (!wtGroups[wt]) wtGroups[wt] = {};
|
||||
const task = i.task_name || '미지정';
|
||||
if (!wtGroups[wt][task]) wtGroups[wt][task] = [];
|
||||
wtGroups[wt][task].push(i);
|
||||
});
|
||||
|
||||
container.innerHTML = Object.entries(groups).map(([wt, items]) => `
|
||||
container.innerHTML = Object.entries(wtGroups).map(([wt, taskGroups]) => `
|
||||
<div class="border rounded-lg overflow-hidden">
|
||||
<div class="bg-amber-50 px-4 py-2 font-medium text-sm text-amber-700">
|
||||
<i class="fas fa-hard-hat mr-1"></i>${escapeHtml(wt)}
|
||||
</div>
|
||||
<div class="divide-y">
|
||||
${items.map(i => renderItemRow(i)).join('')}
|
||||
</div>
|
||||
${Object.entries(taskGroups).map(([task, items]) => `
|
||||
<div class="border-t">
|
||||
<div class="bg-amber-50/50 px-4 py-1.5 text-xs font-medium text-amber-600 pl-8">
|
||||
<i class="fas fa-wrench mr-1"></i>${escapeHtml(task)}
|
||||
</div>
|
||||
<div class="divide-y">
|
||||
${items.map(i => renderItemRow(i)).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
@@ -180,7 +215,7 @@ function openAddItem(tab) {
|
||||
document.getElementById('itemModal').classList.remove('hidden');
|
||||
}
|
||||
|
||||
function openEditItem(id) {
|
||||
async function openEditItem(id) {
|
||||
const item = checklistItems.find(i => i.item_id === id);
|
||||
if (!item) return;
|
||||
editingItemId = id;
|
||||
@@ -196,6 +231,11 @@ function openEditItem(id) {
|
||||
}
|
||||
if (item.work_type_id) {
|
||||
document.getElementById('itemWorkType').value = item.work_type_id;
|
||||
// Load tasks first, then set value after load completes
|
||||
await loadTasksByWorkType(item.work_type_id);
|
||||
if (item.task_id) {
|
||||
document.getElementById('itemTask').value = item.task_id;
|
||||
}
|
||||
}
|
||||
|
||||
toggleTypeFields();
|
||||
@@ -204,6 +244,7 @@ function openEditItem(id) {
|
||||
|
||||
function closeItemModal() {
|
||||
document.getElementById('itemModal').classList.add('hidden');
|
||||
document.getElementById('taskField').classList.add('hidden');
|
||||
editingItemId = null;
|
||||
}
|
||||
|
||||
@@ -211,6 +252,16 @@ function toggleTypeFields() {
|
||||
const type = document.getElementById('itemType').value;
|
||||
document.getElementById('weatherConditionField').classList.toggle('hidden', type !== 'weather');
|
||||
document.getElementById('workTypeField').classList.toggle('hidden', type !== 'work_type');
|
||||
// Hide task field when type changes away from work_type
|
||||
if (type !== 'work_type') {
|
||||
document.getElementById('taskField').classList.add('hidden');
|
||||
} else {
|
||||
// Show task field if work type is already selected
|
||||
const workTypeId = document.getElementById('itemWorkType').value;
|
||||
if (workTypeId) {
|
||||
document.getElementById('taskField').classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== Submit item ===== */
|
||||
@@ -231,8 +282,9 @@ async function submitItem(e) {
|
||||
if (!data.weather_condition_id) { showToast('날씨 조건을 선택해주세요', 'error'); return; }
|
||||
}
|
||||
if (data.item_type === 'work_type') {
|
||||
data.work_type_id = parseInt(document.getElementById('itemWorkType').value) || null;
|
||||
if (!data.work_type_id) { showToast('작업 유형을 선택해주세요', 'error'); return; }
|
||||
const taskId = parseInt(document.getElementById('itemTask').value) || null;
|
||||
if (!taskId) { showToast('작업을 선택해주세요', 'error'); return; }
|
||||
data.task_id = taskId;
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -279,6 +331,7 @@ function initChecklistPage() {
|
||||
|
||||
// Type change handler
|
||||
document.getElementById('itemType').addEventListener('change', toggleTypeFields);
|
||||
document.getElementById('itemWorkType').addEventListener('change', onWorkTypeChange);
|
||||
document.getElementById('itemForm').addEventListener('submit', submitItem);
|
||||
|
||||
loadLookupData();
|
||||
|
||||
Reference in New Issue
Block a user