From 07aac305d6681d05275a6e99b66a1e4c6d8e8c12 Mon Sep 17 00:00:00 2001 From: Hyungi Ahn Date: Fri, 13 Mar 2026 21:07:29 +0900 Subject: [PATCH] =?UTF-8?q?feat(tksafety):=20=EC=B2=B4=ED=81=AC=EB=A6=AC?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=9E=91=EC=97=85=EB=B3=84=20=ED=95=AD?= =?UTF-8?q?=EB=AA=A9=EC=97=90=20tkuser=20=EC=9E=91=EC=97=85(task)=20?= =?UTF-8?q?=EC=B0=B8=EC=A1=B0=20=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .../api/controllers/checklistController.js | 2 +- tksafety/api/models/checklistModel.js | 38 ++++++--- tksafety/web/checklist.html | 11 ++- tksafety/web/static/js/tksafety-checklist.js | 77 ++++++++++++++++--- 4 files changed, 104 insertions(+), 24 deletions(-) diff --git a/tksafety/api/controllers/checklistController.js b/tksafety/api/controllers/checklistController.js index ddbb55c..7f1d3d6 100644 --- a/tksafety/api/controllers/checklistController.js +++ b/tksafety/api/controllers/checklistController.js @@ -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) { diff --git a/tksafety/api/models/checklistModel.js b/tksafety/api/models/checklistModel.js index c03446f..a89cb7a 100644 --- a/tksafety/api/models/checklistModel.js +++ b/tksafety/api/models/checklistModel.js @@ -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; } diff --git a/tksafety/web/checklist.html b/tksafety/web/checklist.html index 2c29812..7ab3160 100644 --- a/tksafety/web/checklist.html +++ b/tksafety/web/checklist.html @@ -122,11 +122,18 @@ + +
@@ -155,7 +162,7 @@
- + diff --git a/tksafety/web/static/js/tksafety-checklist.js b/tksafety/web/static/js/tksafety-checklist.js index bfefcdd..456ea01 100644 --- a/tksafety/web/static/js/tksafety-checklist.js +++ b/tksafety/web/static/js/tksafety-checklist.js @@ -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 = ''; + document.getElementById('taskField').classList.add('hidden'); + return; + } + try { + const res = await api('/checklist/tasks/' + workTypeId); + const tasks = res.data || []; + taskSelect.innerHTML = '' + + tasks.map(t => ``).join(''); + document.getElementById('taskField').classList.remove('hidden'); + } catch (e) { + console.error('Task load error:', e); + taskSelect.innerHTML = ''; + } +} + +/* ===== 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]) => `
${escapeHtml(wt)}
-
- ${items.map(i => renderItemRow(i)).join('')} -
+ ${Object.entries(taskGroups).map(([task, items]) => ` +
+
+ ${escapeHtml(task)} +
+
+ ${items.map(i => renderItemRow(i)).join('')} +
+
+ `).join('')}
`).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();