feat: 작업 관리 시스템 및 TBM 공정/작업 통합
## Backend Changes - Create tasks table with work_type_id FK to work_types - Add taskModel, taskController, taskRoutes for task CRUD - Update tbmModel to support work_type_id and task_id - Add migrations for tasks table and TBM integration ## Frontend Changes - Create task management admin page (tasks.html, task-management.js) - Update TBM modal to include work type (공정) and task (작업) selection - Add cascading dropdown: work type → task selection - Display work type and task info in TBM session cards - Update sidebar navigation in all admin pages ## Database Schema - tasks: task_id, work_type_id, task_name, description, is_active - tbm_sessions: add work_type_id, task_id columns with FKs - Foreign keys maintain referential integrity with work_types and tasks 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -41,6 +41,7 @@ function setupRoutes(app) {
|
||||
const monthlyStatusRoutes = require('../routes/monthlyStatusRoutes');
|
||||
const pageAccessRoutes = require('../routes/pageAccessRoutes');
|
||||
const workplaceRoutes = require('../routes/workplaceRoutes');
|
||||
const taskRoutes = require('../routes/taskRoutes');
|
||||
// const tbmRoutes = require('../routes/tbmRoutes'); // 임시 비활성화 - db/connection 문제
|
||||
|
||||
// Rate Limiters 설정
|
||||
@@ -129,6 +130,7 @@ function setupRoutes(app) {
|
||||
app.use('/api/tools', toolsRoute);
|
||||
app.use('/api/users', userRoutes);
|
||||
app.use('/api/workplaces', workplaceRoutes);
|
||||
app.use('/api/tasks', taskRoutes);
|
||||
app.use('/api', pageAccessRoutes); // 페이지 접근 권한 관리
|
||||
// app.use('/api/tbm', tbmRoutes); // TBM 시스템 - 임시 비활성화
|
||||
app.use('/api', uploadBgRoutes);
|
||||
|
||||
179
api.hyungi.net/controllers/taskController.js
Normal file
179
api.hyungi.net/controllers/taskController.js
Normal file
@@ -0,0 +1,179 @@
|
||||
/**
|
||||
* 작업 관리 컨트롤러
|
||||
*
|
||||
* 작업 CRUD API 엔드포인트 핸들러
|
||||
* (공정=work_types에 속하는 세부 작업)
|
||||
*
|
||||
* @author TK-FB-Project
|
||||
* @since 2026-01-26
|
||||
*/
|
||||
|
||||
const taskModel = require('../models/taskModel');
|
||||
const { ValidationError, NotFoundError, DatabaseError } = require('../utils/errors');
|
||||
const { asyncHandler } = require('../middlewares/errorHandler');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
// ==================== 작업 CRUD ====================
|
||||
|
||||
/**
|
||||
* 작업 생성
|
||||
*/
|
||||
exports.createTask = asyncHandler(async (req, res) => {
|
||||
const taskData = req.body;
|
||||
|
||||
if (!taskData.task_name) {
|
||||
throw new ValidationError('작업명은 필수 입력 항목입니다');
|
||||
}
|
||||
|
||||
logger.info('작업 생성 요청', { name: taskData.task_name });
|
||||
|
||||
const id = await new Promise((resolve, reject) => {
|
||||
taskModel.createTask(taskData, (err, lastID) => {
|
||||
if (err) reject(new DatabaseError('작업 생성 중 오류가 발생했습니다'));
|
||||
else resolve(lastID);
|
||||
});
|
||||
});
|
||||
|
||||
logger.info('작업 생성 성공', { task_id: id });
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: { task_id: id },
|
||||
message: '작업이 성공적으로 생성되었습니다'
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 전체 작업 조회
|
||||
*/
|
||||
exports.getAllTasks = asyncHandler(async (req, res) => {
|
||||
const rows = await new Promise((resolve, reject) => {
|
||||
taskModel.getAllTasks((err, data) => {
|
||||
if (err) reject(new DatabaseError('작업 목록 조회 중 오류가 발생했습니다'));
|
||||
else resolve(data);
|
||||
});
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: rows,
|
||||
message: '작업 목록 조회 성공'
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 활성 작업만 조회
|
||||
*/
|
||||
exports.getActiveTasks = asyncHandler(async (req, res) => {
|
||||
const rows = await new Promise((resolve, reject) => {
|
||||
taskModel.getActiveTasks((err, data) => {
|
||||
if (err) reject(new DatabaseError('활성 작업 목록 조회 중 오류가 발생했습니다'));
|
||||
else resolve(data);
|
||||
});
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: rows,
|
||||
message: '활성 작업 목록 조회 성공'
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 공정별 작업 조회
|
||||
*/
|
||||
exports.getTasksByWorkType = asyncHandler(async (req, res) => {
|
||||
const workTypeId = req.params.work_type_id || req.query.work_type_id;
|
||||
|
||||
if (!workTypeId) {
|
||||
throw new ValidationError('공정 ID가 필요합니다');
|
||||
}
|
||||
|
||||
const rows = await new Promise((resolve, reject) => {
|
||||
taskModel.getTasksByWorkType(workTypeId, (err, data) => {
|
||||
if (err) reject(new DatabaseError('공정별 작업 목록 조회 중 오류가 발생했습니다'));
|
||||
else resolve(data);
|
||||
});
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: rows,
|
||||
message: '공정별 작업 목록 조회 성공'
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 단일 작업 조회
|
||||
*/
|
||||
exports.getTaskById = asyncHandler(async (req, res) => {
|
||||
const taskId = req.params.id;
|
||||
|
||||
const task = await new Promise((resolve, reject) => {
|
||||
taskModel.getTaskById(taskId, (err, data) => {
|
||||
if (err) reject(new DatabaseError('작업 조회 중 오류가 발생했습니다'));
|
||||
else resolve(data);
|
||||
});
|
||||
});
|
||||
|
||||
if (!task) {
|
||||
throw new NotFoundError('작업을 찾을 수 없습니다');
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: task,
|
||||
message: '작업 조회 성공'
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 작업 수정
|
||||
*/
|
||||
exports.updateTask = asyncHandler(async (req, res) => {
|
||||
const taskId = req.params.id;
|
||||
const taskData = req.body;
|
||||
|
||||
if (!taskData.task_name) {
|
||||
throw new ValidationError('작업명은 필수 입력 항목입니다');
|
||||
}
|
||||
|
||||
logger.info('작업 수정 요청', { task_id: taskId });
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
taskModel.updateTask(taskId, taskData, (err, result) => {
|
||||
if (err) reject(new DatabaseError('작업 수정 중 오류가 발생했습니다'));
|
||||
else resolve(result);
|
||||
});
|
||||
});
|
||||
|
||||
logger.info('작업 수정 성공', { task_id: taskId });
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '작업이 성공적으로 수정되었습니다'
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 작업 삭제
|
||||
*/
|
||||
exports.deleteTask = asyncHandler(async (req, res) => {
|
||||
const taskId = req.params.id;
|
||||
|
||||
logger.info('작업 삭제 요청', { task_id: taskId });
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
taskModel.deleteTask(taskId, (err, result) => {
|
||||
if (err) reject(new DatabaseError('작업 삭제 중 오류가 발생했습니다'));
|
||||
else resolve(result);
|
||||
});
|
||||
});
|
||||
|
||||
logger.info('작업 삭제 성공', { task_id: taskId });
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '작업이 성공적으로 삭제되었습니다'
|
||||
});
|
||||
});
|
||||
36
api.hyungi.net/db/migrations/20260126010002_create_tasks.js
Normal file
36
api.hyungi.net/db/migrations/20260126010002_create_tasks.js
Normal file
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* 작업 테이블 생성 (공정=work_types에 속함)
|
||||
*
|
||||
* @param {import('knex').Knex} knex
|
||||
*/
|
||||
exports.up = function(knex) {
|
||||
return knex.schema.createTable('tasks', function(table) {
|
||||
table.increments('task_id').primary().comment('작업 ID');
|
||||
table.integer('work_type_id').nullable().comment('공정 ID (work_types 참조)');
|
||||
table.string('task_name', 255).notNullable().comment('작업명');
|
||||
table.text('description').nullable().comment('작업 설명');
|
||||
table.boolean('is_active').defaultTo(true).comment('활성화 여부');
|
||||
table.timestamp('created_at').defaultTo(knex.fn.now()).comment('생성일시');
|
||||
table.timestamp('updated_at').defaultTo(knex.fn.now()).comment('수정일시');
|
||||
|
||||
// 외래키 (work_types 테이블 참조)
|
||||
table.foreign('work_type_id')
|
||||
.references('id')
|
||||
.inTable('work_types')
|
||||
.onDelete('SET NULL')
|
||||
.onUpdate('CASCADE');
|
||||
|
||||
// 인덱스
|
||||
table.index('work_type_id');
|
||||
table.index('is_active');
|
||||
}).then(() => {
|
||||
console.log('✅ tasks 테이블 생성 완료');
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {import('knex').Knex} knex
|
||||
*/
|
||||
exports.down = function(knex) {
|
||||
return knex.schema.dropTableIfExists('tasks');
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* TBM 세션에 공정/작업 컬럼 추가
|
||||
*
|
||||
* @param {import('knex').Knex} knex
|
||||
*/
|
||||
exports.up = function(knex) {
|
||||
return knex.schema.table('tbm_sessions', function(table) {
|
||||
table.integer('work_type_id').nullable().comment('공정 ID (work_types 참조)');
|
||||
table.integer('task_id').unsigned().nullable().comment('작업 ID (tasks 참조)');
|
||||
|
||||
// 외래키 추가
|
||||
table.foreign('work_type_id')
|
||||
.references('id')
|
||||
.inTable('work_types')
|
||||
.onDelete('SET NULL')
|
||||
.onUpdate('CASCADE');
|
||||
|
||||
table.foreign('task_id')
|
||||
.references('task_id')
|
||||
.inTable('tasks')
|
||||
.onDelete('SET NULL')
|
||||
.onUpdate('CASCADE');
|
||||
|
||||
// 인덱스 추가
|
||||
table.index('work_type_id');
|
||||
table.index('task_id');
|
||||
}).then(() => {
|
||||
console.log('✅ tbm_sessions 테이블에 work_type_id, task_id 컬럼 추가 완료');
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {import('knex').Knex} knex
|
||||
*/
|
||||
exports.down = function(knex) {
|
||||
return knex.schema.table('tbm_sessions', function(table) {
|
||||
table.dropForeign('work_type_id');
|
||||
table.dropForeign('task_id');
|
||||
table.dropColumn('work_type_id');
|
||||
table.dropColumn('task_id');
|
||||
});
|
||||
};
|
||||
170
api.hyungi.net/models/taskModel.js
Normal file
170
api.hyungi.net/models/taskModel.js
Normal file
@@ -0,0 +1,170 @@
|
||||
/**
|
||||
* 작업 모델
|
||||
*
|
||||
* @author TK-FB-Project
|
||||
* @since 2026-01-26
|
||||
*/
|
||||
|
||||
const { getDb } = require('../db/connection');
|
||||
|
||||
// ==================== 작업 CRUD ====================
|
||||
|
||||
/**
|
||||
* 작업 생성
|
||||
*/
|
||||
const createTask = async (taskData, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const { work_type_id, task_name, description } = taskData;
|
||||
|
||||
const [result] = await db.query(
|
||||
`INSERT INTO tasks (work_type_id, task_name, description, is_active)
|
||||
VALUES (?, ?, ?, 1)`,
|
||||
[work_type_id || null, task_name, description || null]
|
||||
);
|
||||
|
||||
callback(null, result.insertId);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 전체 작업 목록 조회 (공정 정보 포함)
|
||||
*/
|
||||
const getAllTasks = async (callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(
|
||||
`SELECT t.task_id, t.work_type_id, t.task_name, t.description, t.is_active,
|
||||
t.created_at, t.updated_at,
|
||||
wt.name as work_type_name, wt.category
|
||||
FROM tasks t
|
||||
LEFT JOIN work_types wt ON t.work_type_id = wt.id
|
||||
ORDER BY wt.category ASC, t.task_id DESC`
|
||||
);
|
||||
callback(null, rows);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 활성 작업만 조회
|
||||
*/
|
||||
const getActiveTasks = async (callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(
|
||||
`SELECT t.task_id, t.work_type_id, t.task_name, t.description,
|
||||
wt.name as work_type_name, wt.category
|
||||
FROM tasks t
|
||||
LEFT JOIN work_types wt ON t.work_type_id = wt.id
|
||||
WHERE t.is_active = 1
|
||||
ORDER BY wt.category ASC, t.task_name ASC`
|
||||
);
|
||||
callback(null, rows);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 공정별 작업 목록 조회
|
||||
*/
|
||||
const getTasksByWorkType = async (workTypeId, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(
|
||||
`SELECT t.task_id, t.work_type_id, t.task_name, t.description, t.is_active,
|
||||
t.created_at, t.updated_at,
|
||||
wt.name as work_type_name, wt.category
|
||||
FROM tasks t
|
||||
LEFT JOIN work_types wt ON t.work_type_id = wt.id
|
||||
WHERE t.work_type_id = ?
|
||||
ORDER BY t.task_id DESC`,
|
||||
[workTypeId]
|
||||
);
|
||||
callback(null, rows);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 단일 작업 조회
|
||||
*/
|
||||
const getTaskById = async (taskId, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(
|
||||
`SELECT t.task_id, t.work_type_id, t.task_name, t.description, t.is_active,
|
||||
t.created_at, t.updated_at,
|
||||
wt.name as work_type_name, wt.category
|
||||
FROM tasks t
|
||||
LEFT JOIN work_types wt ON t.work_type_id = wt.id
|
||||
WHERE t.task_id = ?`,
|
||||
[taskId]
|
||||
);
|
||||
callback(null, rows[0] || null);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 작업 수정
|
||||
*/
|
||||
const updateTask = async (taskId, taskData, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const { work_type_id, task_name, description, is_active } = taskData;
|
||||
|
||||
const [result] = await db.query(
|
||||
`UPDATE tasks
|
||||
SET work_type_id = ?,
|
||||
task_name = ?,
|
||||
description = ?,
|
||||
is_active = ?,
|
||||
updated_at = NOW()
|
||||
WHERE task_id = ?`,
|
||||
[
|
||||
work_type_id || null,
|
||||
task_name,
|
||||
description || null,
|
||||
is_active !== undefined ? is_active : 1,
|
||||
taskId
|
||||
]
|
||||
);
|
||||
|
||||
callback(null, result);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 작업 삭제
|
||||
*/
|
||||
const deleteTask = async (taskId, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [result] = await db.query(
|
||||
`DELETE FROM tasks WHERE task_id = ?`,
|
||||
[taskId]
|
||||
);
|
||||
callback(null, result);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
createTask,
|
||||
getAllTasks,
|
||||
getActiveTasks,
|
||||
getTasksByWorkType,
|
||||
getTaskById,
|
||||
updateTask,
|
||||
deleteTask
|
||||
};
|
||||
@@ -10,15 +10,17 @@ const TbmModel = {
|
||||
createSession: (sessionData, callback) => {
|
||||
const sql = `
|
||||
INSERT INTO tbm_sessions
|
||||
(session_date, leader_id, project_id, work_location, work_description,
|
||||
safety_notes, start_time, created_by)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
(session_date, leader_id, project_id, work_type_id, task_id, work_location,
|
||||
work_description, safety_notes, start_time, created_by)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`;
|
||||
|
||||
const values = [
|
||||
sessionData.session_date,
|
||||
sessionData.leader_id,
|
||||
sessionData.project_id,
|
||||
sessionData.work_type_id,
|
||||
sessionData.task_id,
|
||||
sessionData.work_location,
|
||||
sessionData.work_description,
|
||||
sessionData.safety_notes,
|
||||
@@ -40,11 +42,16 @@ const TbmModel = {
|
||||
w.job_type as leader_job_type,
|
||||
p.project_name,
|
||||
p.job_no,
|
||||
wt.name as work_type_name,
|
||||
wt.category as work_type_category,
|
||||
t.task_name,
|
||||
u.username as created_by_username,
|
||||
COUNT(DISTINCT ta.worker_id) as team_member_count
|
||||
FROM tbm_sessions s
|
||||
LEFT JOIN workers w ON s.leader_id = w.worker_id
|
||||
LEFT JOIN projects p ON s.project_id = p.project_id
|
||||
LEFT JOIN work_types wt ON s.work_type_id = wt.id
|
||||
LEFT JOIN tasks t ON s.task_id = t.task_id
|
||||
LEFT JOIN users u ON s.created_by = u.user_id
|
||||
LEFT JOIN tbm_team_assignments ta ON s.session_id = ta.session_id
|
||||
WHERE s.session_date = ?
|
||||
@@ -68,11 +75,17 @@ const TbmModel = {
|
||||
p.project_name,
|
||||
p.job_no,
|
||||
p.site,
|
||||
wt.name as work_type_name,
|
||||
wt.category as work_type_category,
|
||||
t.task_name,
|
||||
t.description as task_description,
|
||||
u.username as created_by_username,
|
||||
u.name as created_by_name
|
||||
FROM tbm_sessions s
|
||||
LEFT JOIN workers w ON s.leader_id = w.worker_id
|
||||
LEFT JOIN projects p ON s.project_id = p.project_id
|
||||
LEFT JOIN work_types wt ON s.work_type_id = wt.id
|
||||
LEFT JOIN tasks t ON s.task_id = t.task_id
|
||||
LEFT JOIN users u ON s.created_by = u.user_id
|
||||
WHERE s.session_id = ?
|
||||
`;
|
||||
|
||||
27
api.hyungi.net/routes/taskRoutes.js
Normal file
27
api.hyungi.net/routes/taskRoutes.js
Normal file
@@ -0,0 +1,27 @@
|
||||
// routes/taskRoutes.js
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const taskController = require('../controllers/taskController');
|
||||
|
||||
// CREATE 작업
|
||||
router.post('/', taskController.createTask);
|
||||
|
||||
// READ ALL 작업
|
||||
router.get('/', taskController.getAllTasks);
|
||||
|
||||
// READ ACTIVE 작업
|
||||
router.get('/active/list', taskController.getActiveTasks);
|
||||
|
||||
// READ BY WORK TYPE (공정별)
|
||||
router.get('/by-work-type/:work_type_id', taskController.getTasksByWorkType);
|
||||
|
||||
// READ ONE 작업
|
||||
router.get('/:id', taskController.getTaskById);
|
||||
|
||||
// UPDATE 작업
|
||||
router.put('/:id', taskController.updateTask);
|
||||
|
||||
// DELETE 작업
|
||||
router.delete('/:id', taskController.deleteTask);
|
||||
|
||||
module.exports = router;
|
||||
Reference in New Issue
Block a user