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;
|
||||
378
web-ui/js/task-management.js
Normal file
378
web-ui/js/task-management.js
Normal file
@@ -0,0 +1,378 @@
|
||||
// task-management.js - 작업 관리 페이지 JavaScript
|
||||
|
||||
// 전역 변수
|
||||
let workTypes = []; // 공정 목록
|
||||
let tasks = []; // 작업 목록
|
||||
let currentWorkTypeId = ''; // 현재 선택된 공정 ID
|
||||
let currentEditingTask = null;
|
||||
|
||||
// 페이지 초기화
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
console.log('📋 작업 관리 페이지 초기화');
|
||||
|
||||
// API 함수가 로드될 때까지 대기
|
||||
let retryCount = 0;
|
||||
while (!window.apiCall && retryCount < 50) {
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
retryCount++;
|
||||
}
|
||||
|
||||
if (!window.apiCall) {
|
||||
showToast('시스템을 초기화할 수 없습니다. 페이지를 새로고침해주세요.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
await loadAllData();
|
||||
});
|
||||
|
||||
// 전체 데이터 로드
|
||||
async function loadAllData() {
|
||||
try {
|
||||
// 공정 목록 로드 (work_types 조회 - 코드 관리 API 사용)
|
||||
await loadWorkTypes();
|
||||
// 작업 목록 로드
|
||||
await loadTasks();
|
||||
} catch (error) {
|
||||
console.error('❌ 데이터 로드 오류:', error);
|
||||
showToast('데이터를 불러오는 중 오류가 발생했습니다.', 'error');
|
||||
}
|
||||
}
|
||||
window.loadAllData = loadAllData;
|
||||
|
||||
// 공정 목록 로드
|
||||
async function loadWorkTypes() {
|
||||
try {
|
||||
// 작업 유형(공정) 목록 조회 - codes API 사용 또는 직접 DB 조회
|
||||
// 간단하게 하드코딩으로 시작 (나중에 API로 변경 가능)
|
||||
const response = await window.apiCall('/tools/work-types');
|
||||
|
||||
if (response && response.success) {
|
||||
workTypes = response.data || [];
|
||||
} else {
|
||||
workTypes = [];
|
||||
}
|
||||
|
||||
console.log('✅ 공정 목록 로드:', workTypes.length + '개');
|
||||
renderWorkTypeTabs();
|
||||
populateWorkTypeSelect();
|
||||
} catch (error) {
|
||||
console.error('❌ 공정 목록 조회 오류:', error);
|
||||
// API 오류 시에도 빈 배열로 처리
|
||||
workTypes = [];
|
||||
renderWorkTypeTabs();
|
||||
}
|
||||
}
|
||||
|
||||
// 작업 목록 로드
|
||||
async function loadTasks() {
|
||||
try {
|
||||
const response = await window.apiCall('/tasks');
|
||||
|
||||
if (response && response.success) {
|
||||
tasks = response.data || [];
|
||||
console.log('✅ 작업 목록 로드:', tasks.length + '개');
|
||||
} else {
|
||||
tasks = [];
|
||||
}
|
||||
|
||||
renderTasks();
|
||||
updateStatistics();
|
||||
} catch (error) {
|
||||
console.error('❌ 작업 목록 조회 오류:', error);
|
||||
showToast('작업 목록을 불러오는 중 오류가 발생했습니다.', 'error');
|
||||
tasks = [];
|
||||
renderTasks();
|
||||
}
|
||||
}
|
||||
|
||||
// 공정 탭 렌더링
|
||||
function renderWorkTypeTabs() {
|
||||
const tabsContainer = document.getElementById('workTypeTabs');
|
||||
|
||||
let tabsHtml = `
|
||||
<button class="tab-btn ${currentWorkTypeId === '' ? 'active' : ''}"
|
||||
data-work-type="" onclick="switchWorkType('')">
|
||||
<span class="tab-icon">📋</span>
|
||||
전체 (${tasks.length})
|
||||
</button>
|
||||
`;
|
||||
|
||||
workTypes.forEach(workType => {
|
||||
const count = tasks.filter(t => t.work_type_id === workType.id).length;
|
||||
const isActive = currentWorkTypeId === workType.id;
|
||||
|
||||
tabsHtml += `
|
||||
<button class="tab-btn ${isActive ? 'active' : ''}"
|
||||
data-work-type="${workType.id}"
|
||||
onclick="switchWorkType(${workType.id})">
|
||||
<span class="tab-icon">🔧</span>
|
||||
${workType.name} (${count})
|
||||
</button>
|
||||
`;
|
||||
});
|
||||
|
||||
tabsContainer.innerHTML = tabsHtml;
|
||||
}
|
||||
|
||||
// 공정 전환
|
||||
function switchWorkType(workTypeId) {
|
||||
currentWorkTypeId = workTypeId === '' ? '' : parseInt(workTypeId);
|
||||
renderWorkTypeTabs();
|
||||
renderTasks();
|
||||
updateStatistics();
|
||||
}
|
||||
window.switchWorkType = switchWorkType;
|
||||
|
||||
// 작업 목록 렌더링
|
||||
function renderTasks() {
|
||||
const grid = document.getElementById('taskGrid');
|
||||
|
||||
// 현재 선택된 공정으로 필터링
|
||||
let filteredTasks = tasks;
|
||||
if (currentWorkTypeId !== '') {
|
||||
filteredTasks = tasks.filter(t => t.work_type_id === currentWorkTypeId);
|
||||
}
|
||||
|
||||
if (filteredTasks.length === 0) {
|
||||
grid.innerHTML = `
|
||||
<div class="empty-state" style="grid-column: 1 / -1;">
|
||||
<div class="empty-icon">📋</div>
|
||||
<h3>등록된 작업이 없습니다</h3>
|
||||
<p>"작업 추가" 버튼을 눌러 새로운 작업을 등록하세요</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
grid.innerHTML = filteredTasks.map(task => createTaskCard(task)).join('');
|
||||
}
|
||||
|
||||
// 작업 카드 생성
|
||||
function createTaskCard(task) {
|
||||
const statusBadge = task.is_active
|
||||
? '<span class="badge" style="background: #dcfce7; color: #166534;">활성</span>'
|
||||
: '<span class="badge" style="background: #f3f4f6; color: #6b7280;">비활성</span>';
|
||||
|
||||
return `
|
||||
<div class="code-card" onclick="editTask(${task.task_id})">
|
||||
<div class="code-card-header">
|
||||
<h3 class="code-name">${task.task_name}</h3>
|
||||
${statusBadge}
|
||||
</div>
|
||||
|
||||
<div class="code-info">
|
||||
<div class="info-item">
|
||||
<span class="info-label">소속 공정</span>
|
||||
<span class="info-value">${task.work_type_name || '-'}</span>
|
||||
</div>
|
||||
${task.category ? `
|
||||
<div class="info-item">
|
||||
<span class="info-label">카테고리</span>
|
||||
<span class="info-value">${task.category}</span>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
|
||||
${task.description ? `
|
||||
<div class="code-description">
|
||||
${task.description}
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<div class="code-meta">
|
||||
<span>등록: ${formatDate(task.created_at)}</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// 통계 업데이트
|
||||
function updateStatistics() {
|
||||
let filteredTasks = tasks;
|
||||
if (currentWorkTypeId !== '') {
|
||||
filteredTasks = tasks.filter(t => t.work_type_id === currentWorkTypeId);
|
||||
}
|
||||
|
||||
const activeCount = filteredTasks.filter(t => t.is_active).length;
|
||||
|
||||
document.getElementById('totalCount').textContent = filteredTasks.length;
|
||||
document.getElementById('activeCount').textContent = activeCount;
|
||||
}
|
||||
|
||||
// 새로고침
|
||||
function refreshTasks() {
|
||||
loadAllData();
|
||||
showToast('데이터를 새로고침했습니다.', 'success');
|
||||
}
|
||||
window.refreshTasks = refreshTasks;
|
||||
|
||||
// ==================== 작업 모달 ====================
|
||||
|
||||
// 작업 모달 열기 (신규)
|
||||
function openTaskModal() {
|
||||
currentEditingTask = null;
|
||||
document.getElementById('taskModalTitle').textContent = '작업 추가';
|
||||
document.getElementById('taskForm').reset();
|
||||
document.getElementById('taskId').value = '';
|
||||
document.getElementById('taskIsActive').checked = true;
|
||||
|
||||
// 공정 선택 드롭다운 채우기
|
||||
populateWorkTypeSelect();
|
||||
|
||||
// 현재 선택된 공정이 있으면 자동 선택
|
||||
if (currentWorkTypeId !== '') {
|
||||
document.getElementById('taskWorkTypeId').value = currentWorkTypeId;
|
||||
}
|
||||
|
||||
document.getElementById('deleteTaskBtn').style.display = 'none';
|
||||
document.getElementById('taskModal').style.display = 'flex';
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
window.openTaskModal = openTaskModal;
|
||||
|
||||
// 작업 편집
|
||||
async function editTask(taskId) {
|
||||
try {
|
||||
const response = await window.apiCall(`/tasks/${taskId}`);
|
||||
|
||||
if (response && response.success) {
|
||||
currentEditingTask = response.data;
|
||||
|
||||
document.getElementById('taskModalTitle').textContent = '작업 수정';
|
||||
document.getElementById('taskId').value = currentEditingTask.task_id;
|
||||
document.getElementById('taskWorkTypeId').value = currentEditingTask.work_type_id || '';
|
||||
document.getElementById('taskName').value = currentEditingTask.task_name;
|
||||
document.getElementById('taskDescription').value = currentEditingTask.description || '';
|
||||
document.getElementById('taskIsActive').checked = currentEditingTask.is_active;
|
||||
|
||||
document.getElementById('deleteTaskBtn').style.display = 'block';
|
||||
document.getElementById('taskModal').style.display = 'flex';
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ 작업 조회 오류:', error);
|
||||
showToast('작업 정보를 불러올 수 없습니다.', 'error');
|
||||
}
|
||||
}
|
||||
window.editTask = editTask;
|
||||
|
||||
// 작업 모달 닫기
|
||||
function closeTaskModal() {
|
||||
document.getElementById('taskModal').style.display = 'none';
|
||||
document.body.style.overflow = 'auto';
|
||||
currentEditingTask = null;
|
||||
}
|
||||
window.closeTaskModal = closeTaskModal;
|
||||
|
||||
// 공정 선택 드롭다운 채우기
|
||||
function populateWorkTypeSelect() {
|
||||
const select = document.getElementById('taskWorkTypeId');
|
||||
|
||||
select.innerHTML = '<option value="">공정 선택...</option>' +
|
||||
workTypes.map(wt => `
|
||||
<option value="${wt.id}">${wt.name}${wt.category ? ' (' + wt.category + ')' : ''}</option>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// 작업 저장
|
||||
async function saveTask() {
|
||||
const taskId = document.getElementById('taskId').value;
|
||||
const taskData = {
|
||||
work_type_id: parseInt(document.getElementById('taskWorkTypeId').value) || null,
|
||||
task_name: document.getElementById('taskName').value.trim(),
|
||||
description: document.getElementById('taskDescription').value.trim() || null,
|
||||
is_active: document.getElementById('taskIsActive').checked ? 1 : 0
|
||||
};
|
||||
|
||||
if (!taskData.task_name) {
|
||||
showToast('작업명을 입력해주세요.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
let response;
|
||||
if (taskId) {
|
||||
// 수정
|
||||
response = await window.apiCall(`/tasks/${taskId}`, 'PUT', taskData);
|
||||
} else {
|
||||
// 신규
|
||||
response = await window.apiCall('/tasks', 'POST', taskData);
|
||||
}
|
||||
|
||||
if (response && response.success) {
|
||||
showToast(taskId ? '작업이 수정되었습니다.' : '작업이 추가되었습니다.', 'success');
|
||||
closeTaskModal();
|
||||
await loadAllData();
|
||||
} else {
|
||||
throw new Error(response.message || '저장에 실패했습니다.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ 작업 저장 오류:', error);
|
||||
showToast('작업 저장 중 오류가 발생했습니다.', 'error');
|
||||
}
|
||||
}
|
||||
window.saveTask = saveTask;
|
||||
|
||||
// 작업 삭제
|
||||
async function deleteTask() {
|
||||
if (!currentEditingTask) return;
|
||||
|
||||
if (!confirm(`"${currentEditingTask.task_name}" 작업을 삭제하시겠습니까?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await window.apiCall(`/tasks/${currentEditingTask.task_id}`, 'DELETE');
|
||||
|
||||
if (response && response.success) {
|
||||
showToast('작업이 삭제되었습니다.', 'success');
|
||||
closeTaskModal();
|
||||
await loadAllData();
|
||||
} else {
|
||||
throw new Error(response.message || '삭제에 실패했습니다.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ 작업 삭제 오류:', error);
|
||||
showToast('작업 삭제 중 오류가 발생했습니다.', 'error');
|
||||
}
|
||||
}
|
||||
window.deleteTask = deleteTask;
|
||||
|
||||
// ==================== 유틸리티 ====================
|
||||
|
||||
// 날짜 포맷
|
||||
function formatDate(dateString) {
|
||||
if (!dateString) return '-';
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('ko-KR', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit'
|
||||
});
|
||||
}
|
||||
|
||||
// 토스트 알림
|
||||
function showToast(message, type = 'info') {
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `toast toast-${type}`;
|
||||
toast.textContent = message;
|
||||
toast.style.cssText = `
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
padding: 1rem 1.5rem;
|
||||
background: ${type === 'success' ? '#10b981' : type === 'error' ? '#ef4444' : '#3b82f6'};
|
||||
color: white;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
z-index: 10000;
|
||||
animation: slideIn 0.3s ease-out;
|
||||
`;
|
||||
|
||||
document.body.appendChild(toast);
|
||||
|
||||
setTimeout(() => {
|
||||
toast.style.animation = 'slideOut 0.3s ease-out';
|
||||
setTimeout(() => toast.remove(), 300);
|
||||
}, 3000);
|
||||
}
|
||||
@@ -5,6 +5,8 @@ let allSessions = [];
|
||||
let todaySessions = [];
|
||||
let allWorkers = [];
|
||||
let allProjects = [];
|
||||
let allWorkTypes = [];
|
||||
let allTasks = [];
|
||||
let allSafetyChecks = [];
|
||||
let currentSessionId = null;
|
||||
let selectedWorkers = new Set();
|
||||
@@ -77,6 +79,20 @@ async function loadInitialData() {
|
||||
console.log('✅ 안전 체크리스트 로드:', allSafetyChecks.length + '개');
|
||||
}
|
||||
|
||||
// 공정(Work Types) 목록 로드
|
||||
const workTypesResponse = await window.apiCall('/tools/work-types');
|
||||
if (workTypesResponse && workTypesResponse.success) {
|
||||
allWorkTypes = workTypesResponse.data || [];
|
||||
console.log('✅ 공정 목록 로드:', allWorkTypes.length + '개');
|
||||
}
|
||||
|
||||
// 작업(Tasks) 목록 로드
|
||||
const tasksResponse = await window.apiCall('/tasks/active/list');
|
||||
if (tasksResponse && tasksResponse.success) {
|
||||
allTasks = tasksResponse.data || [];
|
||||
console.log('✅ 작업 목록 로드:', allTasks.length + '개');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 초기 데이터 로드 오류:', error);
|
||||
showToast('데이터를 불러오는 중 오류가 발생했습니다.', 'error');
|
||||
@@ -275,6 +291,14 @@ function createSessionCard(session) {
|
||||
<span class="info-label">프로젝트</span>
|
||||
<span class="info-value">${session.project_name || '-'}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">공정</span>
|
||||
<span class="info-value">${session.work_type_name || '-'}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">작업</span>
|
||||
<span class="info-value">${session.task_name || '-'}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">작업 장소</span>
|
||||
<span class="info-value">${session.work_location || '-'}</span>
|
||||
@@ -328,6 +352,14 @@ function openNewTbmModal() {
|
||||
// 팀장 목록 로드
|
||||
populateLeaderSelect();
|
||||
populateProjectSelect();
|
||||
populateWorkTypeSelect();
|
||||
|
||||
// 작업 드롭다운 초기화
|
||||
const taskSelect = document.getElementById('taskId');
|
||||
if (taskSelect) {
|
||||
taskSelect.innerHTML = '<option value="">작업 선택...</option>';
|
||||
taskSelect.disabled = true;
|
||||
}
|
||||
|
||||
document.getElementById('tbmModal').style.display = 'flex';
|
||||
document.body.style.overflow = 'hidden';
|
||||
@@ -360,6 +392,48 @@ function populateProjectSelect() {
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// 공정(Work Type) 선택 드롭다운 채우기
|
||||
function populateWorkTypeSelect() {
|
||||
const workTypeSelect = document.getElementById('workTypeId');
|
||||
if (!workTypeSelect) return;
|
||||
|
||||
workTypeSelect.innerHTML = '<option value="">공정 선택...</option>' +
|
||||
allWorkTypes.map(wt => `
|
||||
<option value="${wt.id}">${wt.name}${wt.category ? ' (' + wt.category + ')' : ''}</option>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// 작업(Task) 선택 드롭다운 채우기 (공정 선택 시 호출)
|
||||
function loadTasksByWorkType() {
|
||||
const workTypeId = document.getElementById('workTypeId').value;
|
||||
const taskSelect = document.getElementById('taskId');
|
||||
|
||||
if (!taskSelect) return;
|
||||
|
||||
if (!workTypeId) {
|
||||
taskSelect.innerHTML = '<option value="">작업 선택...</option>';
|
||||
taskSelect.disabled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// 선택한 공정에 해당하는 작업만 필터링
|
||||
const filteredTasks = allTasks.filter(task =>
|
||||
task.work_type_id === parseInt(workTypeId)
|
||||
);
|
||||
|
||||
taskSelect.disabled = false;
|
||||
taskSelect.innerHTML = '<option value="">작업 선택...</option>' +
|
||||
filteredTasks.map(task => `
|
||||
<option value="${task.task_id}">${task.task_name}</option>
|
||||
`).join('');
|
||||
|
||||
if (filteredTasks.length === 0) {
|
||||
taskSelect.innerHTML = '<option value="">등록된 작업이 없습니다</option>';
|
||||
taskSelect.disabled = true;
|
||||
}
|
||||
}
|
||||
window.loadTasksByWorkType = loadTasksByWorkType;
|
||||
|
||||
// TBM 모달 닫기
|
||||
function closeTbmModal() {
|
||||
document.getElementById('tbmModal').style.display = 'none';
|
||||
@@ -369,10 +443,15 @@ window.closeTbmModal = closeTbmModal;
|
||||
|
||||
// TBM 세션 저장
|
||||
async function saveTbmSession() {
|
||||
const workTypeId = document.getElementById('workTypeId').value;
|
||||
const taskId = document.getElementById('taskId').value;
|
||||
|
||||
const sessionData = {
|
||||
session_date: document.getElementById('sessionDate').value,
|
||||
leader_id: parseInt(document.getElementById('leaderId').value),
|
||||
project_id: document.getElementById('projectId').value || null,
|
||||
work_type_id: workTypeId ? parseInt(workTypeId) : null,
|
||||
task_id: taskId ? parseInt(taskId) : null,
|
||||
work_location: document.getElementById('workLocation').value || null,
|
||||
work_description: document.getElementById('workDescription').value || null,
|
||||
safety_notes: document.getElementById('safetyNotes').value || null,
|
||||
@@ -384,6 +463,11 @@ async function saveTbmSession() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!sessionData.work_type_id || !sessionData.task_id) {
|
||||
showToast('공정과 작업을 선택해주세요.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await window.apiCall('/tbm/sessions', 'POST', sessionData);
|
||||
|
||||
|
||||
@@ -41,6 +41,12 @@
|
||||
<span class="menu-text">작업장 관리</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="menu-item">
|
||||
<a href="/pages/admin/tasks.html">
|
||||
<span class="menu-icon">📋</span>
|
||||
<span class="menu-text">작업 관리</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="menu-item active">
|
||||
<a href="/pages/admin/codes.html">
|
||||
<span class="menu-icon">🏷️</span>
|
||||
|
||||
@@ -40,6 +40,12 @@
|
||||
<span class="menu-text">작업장 관리</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="menu-item">
|
||||
<a href="/pages/admin/tasks.html">
|
||||
<span class="menu-icon">📋</span>
|
||||
<span class="menu-text">작업 관리</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="menu-item">
|
||||
<a href="/pages/admin/codes.html">
|
||||
<span class="menu-icon">🏷️</span>
|
||||
|
||||
184
web-ui/pages/admin/tasks.html
Normal file
184
web-ui/pages/admin/tasks.html
Normal file
@@ -0,0 +1,184 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>작업 관리 | (주)테크니컬코리아</title>
|
||||
<link rel="stylesheet" href="/css/design-system.css">
|
||||
<link rel="stylesheet" href="/css/admin-pages.css?v=7">
|
||||
<link rel="icon" type="image/png" href="/img/favicon.png">
|
||||
<script src="/js/auth-check.js?v=1" defer></script>
|
||||
<script type="module" src="/js/api-config.js?v=3"></script>
|
||||
</head>
|
||||
<body>
|
||||
<!-- 네비게이션 바 -->
|
||||
<div id="navbar-container"></div>
|
||||
|
||||
<!-- 메인 레이아웃: 사이드바 + 콘텐츠 -->
|
||||
<div class="page-container">
|
||||
<!-- 사이드바 -->
|
||||
<aside class="sidebar">
|
||||
<nav class="sidebar-nav">
|
||||
<div class="sidebar-header">
|
||||
<h3 class="sidebar-title">관리 메뉴</h3>
|
||||
</div>
|
||||
<ul class="sidebar-menu">
|
||||
<li class="menu-item">
|
||||
<a href="/pages/admin/projects.html">
|
||||
<span class="menu-icon">📁</span>
|
||||
<span class="menu-text">프로젝트 관리</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="menu-item">
|
||||
<a href="/pages/admin/workers.html">
|
||||
<span class="menu-icon">👥</span>
|
||||
<span class="menu-text">작업자 관리</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="menu-item">
|
||||
<a href="/pages/admin/workplaces.html">
|
||||
<span class="menu-icon">🏗️</span>
|
||||
<span class="menu-text">작업장 관리</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="menu-item active">
|
||||
<a href="/pages/admin/tasks.html">
|
||||
<span class="menu-icon">📋</span>
|
||||
<span class="menu-text">작업 관리</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="menu-item">
|
||||
<a href="/pages/admin/codes.html">
|
||||
<span class="menu-icon">🏷️</span>
|
||||
<span class="menu-text">코드 관리</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="menu-divider"></li>
|
||||
<li class="menu-item">
|
||||
<a href="/pages/dashboard.html">
|
||||
<span class="menu-icon">🏠</span>
|
||||
<span class="menu-text">대시보드로</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<!-- 메인 콘텐츠 -->
|
||||
<main class="main-content">
|
||||
<div class="dashboard-main">
|
||||
<div class="page-header">
|
||||
<div class="page-title-section">
|
||||
<h1 class="page-title">
|
||||
<span class="title-icon">📋</span>
|
||||
작업 관리
|
||||
</h1>
|
||||
<p class="page-description">공정별 세부 작업을 등록하고 관리합니다</p>
|
||||
</div>
|
||||
|
||||
<div class="page-actions">
|
||||
<button class="btn btn-primary" onclick="openTaskModal()">
|
||||
<span class="btn-icon">➕</span>
|
||||
작업 추가
|
||||
</button>
|
||||
<button class="btn btn-secondary" onclick="refreshTasks()">
|
||||
<span class="btn-icon">🔄</span>
|
||||
새로고침
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 공정(work_types) 탭 -->
|
||||
<div class="code-tabs" id="workTypeTabs">
|
||||
<button class="tab-btn active" data-work-type="" onclick="switchWorkType('')">
|
||||
<span class="tab-icon">📋</span>
|
||||
전체
|
||||
</button>
|
||||
<!-- 공정 탭들이 여기에 동적으로 생성됩니다 -->
|
||||
</div>
|
||||
|
||||
<!-- 작업 목록 -->
|
||||
<div class="code-section">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">
|
||||
<span class="section-icon">🔧</span>
|
||||
작업 목록
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="code-stats" id="taskStats">
|
||||
<span class="stat-item">
|
||||
<span class="stat-icon">📋</span>
|
||||
전체 <span id="totalCount">0</span>개
|
||||
</span>
|
||||
<span class="stat-item">
|
||||
<span class="stat-icon">✅</span>
|
||||
활성 <span id="activeCount">0</span>개
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="code-grid" id="taskGrid">
|
||||
<!-- 작업 카드들이 여기에 동적으로 생성됩니다 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- 작업 추가/수정 모달 -->
|
||||
<div id="taskModal" class="modal-overlay" style="display: none;">
|
||||
<div class="modal-container">
|
||||
<div class="modal-header">
|
||||
<h2 id="taskModalTitle">작업 추가</h2>
|
||||
<button class="modal-close-btn" onclick="closeTaskModal()">×</button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<form id="taskForm" onsubmit="event.preventDefault(); saveTask();">
|
||||
<input type="hidden" id="taskId">
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">소속 공정 *</label>
|
||||
<select id="taskWorkTypeId" class="form-control" required>
|
||||
<option value="">공정 선택...</option>
|
||||
<!-- 공정 목록이 동적으로 생성됩니다 -->
|
||||
</select>
|
||||
<small class="form-help">이 작업이 속한 공정을 선택하세요</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">작업명 *</label>
|
||||
<input type="text" id="taskName" class="form-control" placeholder="예: 서스 용접, 프레임 조립" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">설명</label>
|
||||
<textarea id="taskDescription" class="form-control" rows="4" placeholder="작업에 대한 설명을 입력하세요"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" style="display: flex; align-items: center; gap: 0.5rem;">
|
||||
<input type="checkbox" id="taskIsActive" checked style="margin: 0;">
|
||||
<span>활성화</span>
|
||||
</label>
|
||||
<small class="form-help">비활성화하면 TBM 입력 시 표시되지 않습니다</small>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" onclick="closeTaskModal()">취소</button>
|
||||
<button type="button" class="btn btn-danger" id="deleteTaskBtn" onclick="deleteTask()" style="display: none;">
|
||||
🗑️ 삭제
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary" onclick="saveTask()">
|
||||
💾 저장
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module" src="/js/load-navbar.js?v=5"></script>
|
||||
<script type="module" src="/js/task-management.js?v=1"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -41,6 +41,12 @@
|
||||
<span class="menu-text">작업장 관리</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="menu-item">
|
||||
<a href="/pages/admin/tasks.html">
|
||||
<span class="menu-icon">📋</span>
|
||||
<span class="menu-text">작업 관리</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="menu-item">
|
||||
<a href="/pages/admin/codes.html">
|
||||
<span class="menu-icon">🏷️</span>
|
||||
|
||||
@@ -41,6 +41,12 @@
|
||||
<span class="menu-text">작업장 관리</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="menu-item">
|
||||
<a href="/pages/admin/tasks.html">
|
||||
<span class="menu-icon">📋</span>
|
||||
<span class="menu-text">작업 관리</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="menu-item">
|
||||
<a href="/pages/admin/codes.html">
|
||||
<span class="menu-icon">🏷️</span>
|
||||
|
||||
@@ -177,6 +177,21 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label class="form-label">공정 (Work Type) *</label>
|
||||
<select id="workTypeId" class="form-control" onchange="loadTasksByWorkType()" required>
|
||||
<option value="">공정 선택...</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">작업 (Task) *</label>
|
||||
<select id="taskId" class="form-control" required>
|
||||
<option value="">작업 선택...</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">작업 내용</label>
|
||||
<textarea id="workDescription" class="form-control" rows="3" placeholder="오늘 진행할 작업 내용을 입력하세요"></textarea>
|
||||
|
||||
Reference in New Issue
Block a user