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:
Hyungi Ahn
2026-03-13 21:07:29 +09:00
parent e8076a8550
commit 07aac305d6
4 changed files with 104 additions and 24 deletions

View File

@@ -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) {

View File

@@ -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;
}

View File

@@ -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>

View File

@@ -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();