feat: 3-System 분리 프로젝트 초기 코드 작성
TK-FB(공장관리+신고)와 M-Project(부적합관리)를 3개 독립 시스템으로 분리하기 위한 전체 코드 구조 작성. - SSO 인증 서비스 (bcrypt + pbkdf2 이중 해시 지원) - System 1: 공장관리 (TK-FB 기반, 신고 코드 제거) - System 2: 신고 (TK-FB에서 workIssue 코드 추출) - System 3: 부적합관리 (M-Project 기반) - Gateway 포털 (path-based 라우팅) - 통합 docker-compose.yml 및 배포 스크립트 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
596
system1-factory/web/pages/admin/tasks.html
Normal file
596
system1-factory/web/pages/admin/tasks.html
Normal file
@@ -0,0 +1,596 @@
|
||||
<!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=8">
|
||||
<link rel="icon" type="image/png" href="/img/favicon.png">
|
||||
<script src="/js/api-base.js"></script>
|
||||
<script src="/js/app-init.js?v=2" defer></script>
|
||||
<style>
|
||||
.page-wrapper { padding: 1rem 1.5rem; max-width: 1400px; }
|
||||
.page-header {
|
||||
display: flex; justify-content: space-between; align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.page-title { font-size: 1.25rem; font-weight: 600; margin: 0; }
|
||||
.header-controls { display: flex; gap: 0.5rem; align-items: center; }
|
||||
.filter-select {
|
||||
padding: 0.4rem 0.5rem; border: 1px solid #d1d5db;
|
||||
border-radius: 0.25rem; font-size: 0.8rem; min-width: 120px;
|
||||
}
|
||||
.btn {
|
||||
padding: 0.4rem 0.75rem; border: none; border-radius: 0.25rem;
|
||||
cursor: pointer; font-size: 0.8rem;
|
||||
}
|
||||
.btn-primary { background: #3b82f6; color: white; }
|
||||
.btn-outline { background: white; border: 1px solid #d1d5db; }
|
||||
.btn-danger { background: #ef4444; color: white; }
|
||||
|
||||
/* 2열 레이아웃 */
|
||||
.two-col-layout { display: grid; grid-template-columns: 280px 1fr; gap: 1rem; }
|
||||
|
||||
/* 공정 패널 */
|
||||
.work-type-panel {
|
||||
background: white; border: 1px solid #e5e7eb; border-radius: 0.25rem;
|
||||
max-height: calc(100vh - 200px); overflow-y: auto;
|
||||
}
|
||||
.panel-header {
|
||||
padding: 0.75rem; border-bottom: 1px solid #e5e7eb;
|
||||
font-weight: 600; font-size: 0.85rem; background: #f9fafb;
|
||||
display: flex; justify-content: space-between; align-items: center;
|
||||
}
|
||||
.panel-header .btn { padding: 0.25rem 0.5rem; font-size: 0.7rem; }
|
||||
.panel-header .count {
|
||||
background: #e5e7eb; padding: 0.1rem 0.5rem; border-radius: 0.25rem;
|
||||
font-size: 0.75rem; font-weight: 500;
|
||||
}
|
||||
.work-type-list { padding: 0; margin: 0; list-style: none; }
|
||||
.work-type-item {
|
||||
padding: 0.6rem 0.75rem; border-bottom: 1px solid #f3f4f6;
|
||||
cursor: pointer; font-size: 0.8rem;
|
||||
display: flex; justify-content: space-between; align-items: center;
|
||||
}
|
||||
.work-type-item:hover { background: #f9fafb; }
|
||||
.work-type-item.active { background: #dbeafe; color: #1d4ed8; }
|
||||
.work-type-item .count {
|
||||
background: #f3f4f6; padding: 0.1rem 0.4rem; border-radius: 0.25rem;
|
||||
font-size: 0.7rem; color: #6b7280;
|
||||
}
|
||||
.work-type-item.active .count { background: #bfdbfe; color: #1d4ed8; }
|
||||
.work-type-item .edit-btn {
|
||||
opacity: 0; font-size: 0.7rem; padding: 0.2rem 0.4rem;
|
||||
background: white; border: 1px solid #d1d5db; border-radius: 0.2rem;
|
||||
cursor: pointer; margin-left: 0.5rem;
|
||||
}
|
||||
.work-type-item:hover .edit-btn { opacity: 1; }
|
||||
|
||||
/* 작업 테이블 */
|
||||
.task-panel { background: white; border: 1px solid #e5e7eb; border-radius: 0.25rem; }
|
||||
.task-header {
|
||||
padding: 0.75rem; border-bottom: 1px solid #e5e7eb;
|
||||
display: flex; justify-content: space-between; align-items: center;
|
||||
background: #f9fafb;
|
||||
}
|
||||
.task-header-title { font-weight: 600; font-size: 0.85rem; }
|
||||
.task-stats { font-size: 0.75rem; color: #6b7280; }
|
||||
.task-stats span { margin-left: 1rem; }
|
||||
.table-wrapper { max-height: calc(100vh - 280px); overflow-y: auto; }
|
||||
.data-table {
|
||||
width: 100%; border-collapse: collapse; font-size: 0.8rem;
|
||||
}
|
||||
.data-table th, .data-table td {
|
||||
padding: 0.5rem 0.6rem; text-align: left; border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
.data-table th {
|
||||
background: #f9fafb; font-weight: 500; color: #374151;
|
||||
font-size: 0.75rem; position: sticky; top: 0;
|
||||
}
|
||||
.data-table tr:hover { background: #f9fafb; }
|
||||
.data-table tr.inactive { opacity: 0.6; }
|
||||
.task-name { font-weight: 500; }
|
||||
.status-badge {
|
||||
display: inline-block; padding: 0.1rem 0.4rem;
|
||||
border-radius: 0.2rem; font-size: 0.7rem; font-weight: 500;
|
||||
}
|
||||
.status-active { background: #dcfce7; color: #166534; }
|
||||
.status-inactive { background: #f3f4f6; color: #6b7280; }
|
||||
.action-btns { display: flex; gap: 0.25rem; }
|
||||
.action-btns button {
|
||||
padding: 0.2rem 0.4rem; font-size: 0.7rem;
|
||||
border: 1px solid #d1d5db; background: white;
|
||||
border-radius: 0.2rem; cursor: pointer;
|
||||
}
|
||||
.action-btns button:hover { background: #f3f4f6; }
|
||||
.action-btns .btn-edit { color: #3b82f6; }
|
||||
.action-btns .btn-del { color: #ef4444; }
|
||||
.empty-row td { text-align: center; padding: 2rem; color: #6b7280; }
|
||||
.desc-cell { max-width: 250px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: #6b7280; }
|
||||
|
||||
/* 모달 */
|
||||
.modal-overlay {
|
||||
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
|
||||
background: rgba(0,0,0,0.5); display: flex;
|
||||
align-items: center; justify-content: center; z-index: 1000;
|
||||
}
|
||||
.modal-container {
|
||||
background: white; border-radius: 0.5rem; width: 500px;
|
||||
max-width: 95vw; max-height: 90vh; overflow-y: auto;
|
||||
}
|
||||
.modal-header {
|
||||
display: flex; justify-content: space-between; align-items: center;
|
||||
padding: 1rem; border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
.modal-header h2 { font-size: 1rem; font-weight: 600; margin: 0; }
|
||||
.modal-close { background: none; border: none; font-size: 1.25rem; cursor: pointer; color: #6b7280; }
|
||||
.modal-body { padding: 1rem; }
|
||||
.modal-footer {
|
||||
display: flex; justify-content: flex-end; gap: 0.5rem;
|
||||
padding: 1rem; border-top: 1px solid #e5e7eb;
|
||||
}
|
||||
.form-group { margin-bottom: 0.75rem; }
|
||||
.form-label { display: block; font-size: 0.8rem; font-weight: 500; color: #374151; margin-bottom: 0.25rem; }
|
||||
.form-control {
|
||||
width: 100%; padding: 0.4rem 0.5rem; border: 1px solid #d1d5db;
|
||||
border-radius: 0.25rem; font-size: 0.85rem;
|
||||
}
|
||||
.form-check { display: flex; align-items: center; gap: 0.5rem; font-size: 0.85rem; }
|
||||
.form-hint { font-size: 0.7rem; color: #6b7280; margin-top: 0.25rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="has-sidebar">
|
||||
<div id="navbar-container"></div>
|
||||
<div id="sidebar-container"></div>
|
||||
|
||||
<main class="main-content">
|
||||
<div class="page-wrapper">
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">작업 관리</h1>
|
||||
<div class="header-controls">
|
||||
<button class="btn btn-outline" onclick="refreshData()">새로고침</button>
|
||||
<button class="btn btn-primary" onclick="openWorkTypeModal()">+ 공정</button>
|
||||
<button class="btn btn-primary" onclick="openTaskModal()">+ 작업</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="two-col-layout">
|
||||
<!-- 공정 목록 -->
|
||||
<div class="work-type-panel">
|
||||
<div class="panel-header">
|
||||
<span>공정 목록</span>
|
||||
<span class="count" id="totalTaskCount">0</span>
|
||||
</div>
|
||||
<ul class="work-type-list" id="workTypeList">
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- 작업 테이블 -->
|
||||
<div class="task-panel">
|
||||
<div class="task-header">
|
||||
<span class="task-header-title" id="currentWorkTypeName">전체 작업</span>
|
||||
<div class="task-stats">
|
||||
<span>활성 <strong id="activeCount">0</strong></span>
|
||||
<span>비활성 <strong id="inactiveCount">0</strong></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-wrapper">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:30px">#</th>
|
||||
<th>작업명</th>
|
||||
<th>소속 공정</th>
|
||||
<th>설명</th>
|
||||
<th style="width:60px">상태</th>
|
||||
<th style="width:80px">관리</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="taskTableBody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</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" onclick="closeTaskModal()">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="taskForm">
|
||||
<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>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">작업명 *</label>
|
||||
<input type="text" id="taskName" class="form-control" required placeholder="예: 서스 용접">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">설명</label>
|
||||
<textarea id="taskDescription" class="form-control" rows="3" placeholder="작업 설명"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-check">
|
||||
<input type="checkbox" id="taskIsActive" checked>
|
||||
<span>활성화</span>
|
||||
</label>
|
||||
<p class="form-hint">비활성화 시 TBM 입력에서 숨김</p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-outline" onclick="closeTaskModal()">취소</button>
|
||||
<button class="btn btn-danger" id="deleteTaskBtn" onclick="deleteTask()" style="display:none;">삭제</button>
|
||||
<button class="btn btn-primary" onclick="saveTask()">저장</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 공정 모달 -->
|
||||
<div id="workTypeModal" class="modal-overlay" style="display:none;">
|
||||
<div class="modal-container">
|
||||
<div class="modal-header">
|
||||
<h2 id="workTypeModalTitle">공정 추가</h2>
|
||||
<button class="modal-close" onclick="closeWorkTypeModal()">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="workTypeForm">
|
||||
<input type="hidden" id="workTypeId">
|
||||
<div class="form-group">
|
||||
<label class="form-label">공정명 *</label>
|
||||
<input type="text" id="workTypeName" class="form-control" required placeholder="예: Base(구조물)">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">카테고리</label>
|
||||
<input type="text" id="workTypeCategory" class="form-control" placeholder="예: 제작">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">설명</label>
|
||||
<textarea id="workTypeDescription" class="form-control" rows="2" placeholder="공정 설명"></textarea>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-outline" onclick="closeWorkTypeModal()">취소</button>
|
||||
<button class="btn btn-danger" id="deleteWorkTypeBtn" onclick="deleteWorkType()" style="display:none;">삭제</button>
|
||||
<button class="btn btn-primary" onclick="saveWorkType()">저장</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let workTypes = [];
|
||||
let tasks = [];
|
||||
let currentWorkTypeId = '';
|
||||
let currentEditingTask = null;
|
||||
let currentEditingWorkType = null;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
let retryCount = 0;
|
||||
while (!window.apiCall && retryCount < 50) {
|
||||
await new Promise(r => setTimeout(r, 100));
|
||||
retryCount++;
|
||||
}
|
||||
if (!window.apiCall) {
|
||||
alert('시스템 초기화 실패. 페이지를 새로고침하세요.');
|
||||
return;
|
||||
}
|
||||
await loadAllData();
|
||||
});
|
||||
|
||||
async function loadAllData() {
|
||||
try {
|
||||
const [wtRes, taskRes] = await Promise.all([
|
||||
window.apiCall('/daily-work-reports/work-types'),
|
||||
window.apiCall('/tasks')
|
||||
]);
|
||||
workTypes = (wtRes && wtRes.success) ? (wtRes.data || []) : [];
|
||||
tasks = (taskRes && taskRes.success) ? (taskRes.data || []) : [];
|
||||
renderWorkTypeList();
|
||||
renderTasks();
|
||||
} catch (e) {
|
||||
console.error('데이터 로드 오류:', e);
|
||||
}
|
||||
}
|
||||
|
||||
function renderWorkTypeList() {
|
||||
const list = document.getElementById('workTypeList');
|
||||
let html = `
|
||||
<li class="work-type-item ${currentWorkTypeId === '' ? 'active' : ''}" data-id="" onclick="filterByWorkType('')">
|
||||
<span>전체</span>
|
||||
<span class="count">${tasks.length}</span>
|
||||
</li>
|
||||
`;
|
||||
workTypes.forEach(wt => {
|
||||
const count = tasks.filter(t => t.work_type_id === wt.id).length;
|
||||
const isActive = currentWorkTypeId === wt.id;
|
||||
html += `
|
||||
<li class="work-type-item ${isActive ? 'active' : ''}" data-id="${wt.id}" onclick="filterByWorkType(${wt.id})">
|
||||
<span>${escapeHtml(wt.name)}</span>
|
||||
<span class="count">${count}</span>
|
||||
<button class="edit-btn" onclick="event.stopPropagation(); editWorkType(${wt.id})">수정</button>
|
||||
</li>
|
||||
`;
|
||||
});
|
||||
list.innerHTML = html;
|
||||
const totalEl = document.getElementById('totalTaskCount');
|
||||
if (totalEl) totalEl.textContent = tasks.length;
|
||||
}
|
||||
|
||||
function filterByWorkType(id) {
|
||||
currentWorkTypeId = id === '' ? '' : parseInt(id);
|
||||
renderWorkTypeList();
|
||||
renderTasks();
|
||||
|
||||
const wt = workTypes.find(w => w.id === currentWorkTypeId);
|
||||
document.getElementById('currentWorkTypeName').textContent = wt ? wt.name + ' 작업' : '전체 작업';
|
||||
}
|
||||
|
||||
function renderTasks() {
|
||||
const tbody = document.getElementById('taskTableBody');
|
||||
let filtered = tasks;
|
||||
if (currentWorkTypeId !== '') {
|
||||
filtered = tasks.filter(t => t.work_type_id === currentWorkTypeId);
|
||||
}
|
||||
|
||||
const active = filtered.filter(t => t.is_active).length;
|
||||
const inactive = filtered.length - active;
|
||||
document.getElementById('activeCount').textContent = active;
|
||||
document.getElementById('inactiveCount').textContent = inactive;
|
||||
|
||||
if (filtered.length === 0) {
|
||||
tbody.innerHTML = '<tr class="empty-row"><td colspan="6">등록된 작업이 없습니다</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
tbody.innerHTML = filtered.map((t, idx) => {
|
||||
const rowClass = t.is_active ? '' : 'inactive';
|
||||
return `
|
||||
<tr class="${rowClass}">
|
||||
<td>${idx + 1}</td>
|
||||
<td class="task-name">${escapeHtml(t.task_name)}</td>
|
||||
<td>${escapeHtml(t.work_type_name || '-')}</td>
|
||||
<td class="desc-cell" title="${escapeHtml(t.description || '')}">${escapeHtml(t.description || '-')}</td>
|
||||
<td><span class="status-badge ${t.is_active ? 'status-active' : 'status-inactive'}">${t.is_active ? '활성' : '비활성'}</span></td>
|
||||
<td>
|
||||
<div class="action-btns">
|
||||
<button class="btn-edit" onclick="editTask(${t.task_id})">수정</button>
|
||||
<button class="btn-del" onclick="confirmDeleteTask(${t.task_id})">삭제</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
if (!str) return '';
|
||||
return str.replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
||||
}
|
||||
|
||||
async function refreshData() {
|
||||
await loadAllData();
|
||||
showToast('새로고침 완료');
|
||||
}
|
||||
|
||||
// ========== 작업 모달 ==========
|
||||
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';
|
||||
}
|
||||
|
||||
function populateWorkTypeSelect() {
|
||||
const select = document.getElementById('taskWorkTypeId');
|
||||
select.innerHTML = '<option value="">선택...</option>' +
|
||||
workTypes.map(wt => `<option value="${wt.id}">${escapeHtml(wt.name)}</option>`).join('');
|
||||
}
|
||||
|
||||
async function editTask(taskId) {
|
||||
try {
|
||||
const res = await window.apiCall(`/tasks/${taskId}`);
|
||||
if (res && res.success) {
|
||||
currentEditingTask = res.data;
|
||||
document.getElementById('taskModalTitle').textContent = '작업 수정';
|
||||
document.getElementById('taskId').value = currentEditingTask.task_id;
|
||||
populateWorkTypeSelect();
|
||||
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 = 'inline-block';
|
||||
document.getElementById('taskModal').style.display = 'flex';
|
||||
}
|
||||
} catch (e) {
|
||||
showToast('작업 조회 오류', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function closeTaskModal() {
|
||||
document.getElementById('taskModal').style.display = 'none';
|
||||
currentEditingTask = null;
|
||||
}
|
||||
|
||||
async function saveTask() {
|
||||
const taskId = document.getElementById('taskId').value;
|
||||
const data = {
|
||||
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 (!data.task_name) { showToast('작업명을 입력하세요', 'error'); return; }
|
||||
|
||||
try {
|
||||
const res = taskId
|
||||
? await window.apiCall(`/tasks/${taskId}`, 'PUT', data)
|
||||
: await window.apiCall('/tasks', 'POST', data);
|
||||
if (res && res.success) {
|
||||
showToast(taskId ? '수정 완료' : '추가 완료');
|
||||
closeTaskModal();
|
||||
await loadAllData();
|
||||
} else {
|
||||
throw new Error(res?.message || '저장 실패');
|
||||
}
|
||||
} catch (e) {
|
||||
showToast(e.message || '저장 오류', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function confirmDeleteTask(taskId) {
|
||||
const task = tasks.find(t => t.task_id === taskId);
|
||||
if (!task) return;
|
||||
if (confirm(`"${task.task_name}" 작업을 삭제하시겠습니까?`)) {
|
||||
deleteTaskById(taskId);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteTask() {
|
||||
if (currentEditingTask) {
|
||||
confirmDeleteTask(currentEditingTask.task_id);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteTaskById(taskId) {
|
||||
try {
|
||||
const res = await window.apiCall(`/tasks/${taskId}`, 'DELETE');
|
||||
if (res && res.success) {
|
||||
showToast('삭제 완료');
|
||||
closeTaskModal();
|
||||
await loadAllData();
|
||||
} else {
|
||||
throw new Error(res?.message || '삭제 실패');
|
||||
}
|
||||
} catch (e) {
|
||||
showToast(e.message || '삭제 오류', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 공정 모달 ==========
|
||||
function openWorkTypeModal() {
|
||||
currentEditingWorkType = null;
|
||||
document.getElementById('workTypeModalTitle').textContent = '공정 추가';
|
||||
document.getElementById('workTypeForm').reset();
|
||||
document.getElementById('workTypeId').value = '';
|
||||
document.getElementById('deleteWorkTypeBtn').style.display = 'none';
|
||||
document.getElementById('workTypeModal').style.display = 'flex';
|
||||
}
|
||||
|
||||
function editWorkType(id) {
|
||||
const wt = workTypes.find(w => w.id === id);
|
||||
if (!wt) return;
|
||||
currentEditingWorkType = wt;
|
||||
document.getElementById('workTypeModalTitle').textContent = '공정 수정';
|
||||
document.getElementById('workTypeId').value = wt.id;
|
||||
document.getElementById('workTypeName').value = wt.name || '';
|
||||
document.getElementById('workTypeCategory').value = wt.category || '';
|
||||
document.getElementById('workTypeDescription').value = wt.description || '';
|
||||
document.getElementById('deleteWorkTypeBtn').style.display = 'inline-block';
|
||||
document.getElementById('workTypeModal').style.display = 'flex';
|
||||
}
|
||||
|
||||
function closeWorkTypeModal() {
|
||||
document.getElementById('workTypeModal').style.display = 'none';
|
||||
currentEditingWorkType = null;
|
||||
}
|
||||
|
||||
async function saveWorkType() {
|
||||
const id = document.getElementById('workTypeId').value;
|
||||
const data = {
|
||||
name: document.getElementById('workTypeName').value.trim(),
|
||||
category: document.getElementById('workTypeCategory').value.trim() || null,
|
||||
description: document.getElementById('workTypeDescription').value.trim() || null
|
||||
};
|
||||
if (!data.name) { showToast('공정명을 입력하세요', 'error'); return; }
|
||||
|
||||
try {
|
||||
const res = id
|
||||
? await window.apiCall(`/daily-work-reports/work-types/${id}`, 'PUT', data)
|
||||
: await window.apiCall('/daily-work-reports/work-types', 'POST', data);
|
||||
if (res && res.success) {
|
||||
showToast(id ? '수정 완료' : '추가 완료');
|
||||
closeWorkTypeModal();
|
||||
await loadAllData();
|
||||
} else {
|
||||
throw new Error(res?.message || '저장 실패');
|
||||
}
|
||||
} catch (e) {
|
||||
showToast(e.message || '저장 오류', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteWorkType() {
|
||||
if (!currentEditingWorkType) return;
|
||||
const related = tasks.filter(t => t.work_type_id === currentEditingWorkType.id);
|
||||
if (related.length > 0) {
|
||||
showToast(`${related.length}개 작업이 연결되어 삭제 불가`, 'error');
|
||||
return;
|
||||
}
|
||||
if (!confirm(`"${currentEditingWorkType.name}" 공정을 삭제하시겠습니까?`)) return;
|
||||
|
||||
try {
|
||||
const res = await window.apiCall(`/daily-work-reports/work-types/${currentEditingWorkType.id}`, 'DELETE');
|
||||
if (res && res.success) {
|
||||
showToast('삭제 완료');
|
||||
closeWorkTypeModal();
|
||||
currentWorkTypeId = '';
|
||||
await loadAllData();
|
||||
} else {
|
||||
throw new Error(res?.message || '삭제 실패');
|
||||
}
|
||||
} catch (e) {
|
||||
showToast(e.message || '삭제 오류', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function showToast(message, type = 'success') {
|
||||
const existing = document.querySelector('.toast-msg');
|
||||
if (existing) existing.remove();
|
||||
const toast = document.createElement('div');
|
||||
toast.className = 'toast-msg';
|
||||
toast.textContent = message;
|
||||
toast.style.cssText = `
|
||||
position: fixed; top: 20px; right: 20px;
|
||||
padding: 0.75rem 1.25rem; border-radius: 0.25rem;
|
||||
color: white; font-size: 0.85rem; z-index: 2000;
|
||||
background: ${type === 'error' ? '#ef4444' : '#10b981'};
|
||||
`;
|
||||
document.body.appendChild(toast);
|
||||
setTimeout(() => toast.remove(), 2500);
|
||||
}
|
||||
|
||||
// 전역 노출
|
||||
window.filterByWorkType = filterByWorkType;
|
||||
window.openTaskModal = openTaskModal;
|
||||
window.closeTaskModal = closeTaskModal;
|
||||
window.editTask = editTask;
|
||||
window.saveTask = saveTask;
|
||||
window.deleteTask = deleteTask;
|
||||
window.confirmDeleteTask = confirmDeleteTask;
|
||||
window.openWorkTypeModal = openWorkTypeModal;
|
||||
window.closeWorkTypeModal = closeWorkTypeModal;
|
||||
window.editWorkType = editWorkType;
|
||||
window.saveWorkType = saveWorkType;
|
||||
window.deleteWorkType = deleteWorkType;
|
||||
window.refreshData = refreshData;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user