@@ -1211,8 +1282,8 @@
[mainEl, navInner, headerInner].forEach(el => { el.classList.remove(wideClass); el.classList.add(defaultClass); });
}
if (name === 'projects' && !projectsLoaded) loadProjects();
- if (name === 'workers' && !workersLoaded) loadWorkers();
if (name === 'departments' && !departmentsLoaded) loadDepartments();
+ if (name === 'issueTypes' && !issueTypesLoaded) loadIssueTypes();
if (name === 'workplaces' && !workplacesLoaded) loadWorkplaces();
if (name === 'tasks' && !tasksLoaded) loadTasksTab();
if (name === 'vacations' && !vacationsLoaded) loadVacationsTab();
@@ -2087,122 +2158,6 @@
} catch(e) { showToast(e.message, 'error'); }
}
- /* ===== Workers CRUD ===== */
- let workers = [], workersLoaded = false, departmentsForSelect = [];
-
- const JOB_TYPE = { leader: '반장', worker: '작업자' };
- function jobTypeBadge(t) {
- if (t === 'leader') return '
반장';
- if (t === 'worker') return '
작업자';
- return t ? `
${t}` : '';
- }
- function workerStatusBadge(s) {
- if (s === 'inactive') return '
비활성';
- return '
재직';
- }
-
- async function loadDepartmentsForSelect() {
- try {
- const r = await api('/departments'); departmentsForSelect = (r.data || r).filter(d => d.is_active !== 0 && d.is_active !== false);
- populateDeptSelects();
- } catch(e) { console.warn('부서 로드 실패:', e); }
- }
- function populateDeptSelects() {
- ['newWorkerDept','editWorkerDept'].forEach(id => {
- const sel = document.getElementById(id); if (!sel) return;
- const val = sel.value;
- sel.innerHTML = '
';
- departmentsForSelect.forEach(d => { const o = document.createElement('option'); o.value = d.department_id; o.textContent = d.department_name; sel.appendChild(o); });
- sel.value = val;
- });
- }
-
- async function loadWorkers() {
- await loadDepartmentsForSelect();
- try {
- const r = await api('/workers'); workers = r.data || r;
- workersLoaded = true;
- displayWorkers();
- } catch (err) {
- document.getElementById('workerList').innerHTML = `
`;
- }
- }
-
- function displayWorkers() {
- const c = document.getElementById('workerList');
- if (!workers.length) { c.innerHTML = '
등록된 작업자가 없습니다.
'; return; }
- c.innerHTML = workers.map(w => `
-
-
-
${w.worker_name}
-
- ${jobTypeBadge(w.job_type)}
- ${w.department_name ? `${w.department_name}` : ''}
- ${workerStatusBadge(w.status)}
- ${w.phone_number ? `${w.phone_number}` : ''}
-
-
-
-
- ${w.status !== 'inactive' ? `` : ''}
-
-
`).join('');
- }
-
- document.getElementById('addWorkerForm').addEventListener('submit', async e => {
- e.preventDefault();
- try {
- await api('/workers', { method: 'POST', body: JSON.stringify({
- worker_name: document.getElementById('newWorkerName').value.trim(),
- job_type: document.getElementById('newJobType').value || null,
- department_id: document.getElementById('newWorkerDept').value ? parseInt(document.getElementById('newWorkerDept').value) : null,
- phone_number: document.getElementById('newWorkerPhone').value.trim() || null,
- hire_date: document.getElementById('newWorkerHireDate').value || null,
- notes: document.getElementById('newWorkerNotes').value.trim() || null
- })});
- showToast('작업자가 추가되었습니다.'); document.getElementById('addWorkerForm').reset(); await loadWorkers();
- } catch(e) { showToast(e.message, 'error'); }
- });
-
- function editWorker(id) {
- const w = workers.find(x => x.worker_id === id); if (!w) return;
- document.getElementById('editWorkerId').value = w.worker_id;
- document.getElementById('editWorkerName').value = w.worker_name;
- document.getElementById('editJobType').value = w.job_type || '';
- document.getElementById('editWorkerDept').value = w.department_id || '';
- document.getElementById('editWorkerPhone').value = w.phone_number || '';
- document.getElementById('editWorkerHireDate').value = formatDate(w.hire_date);
- document.getElementById('editWorkerNotes').value = w.notes || '';
- document.getElementById('editWorkerStatus').value = w.status || 'active';
- document.getElementById('editEmploymentStatus').value = w.employment_status || 'employed';
- populateDeptSelects();
- document.getElementById('editWorkerDept').value = w.department_id || '';
- document.getElementById('editWorkerModal').classList.remove('hidden');
- }
- function closeWorkerModal() { document.getElementById('editWorkerModal').classList.add('hidden'); }
-
- document.getElementById('editWorkerForm').addEventListener('submit', async e => {
- e.preventDefault();
- try {
- await api(`/workers/${document.getElementById('editWorkerId').value}`, { method: 'PUT', body: JSON.stringify({
- worker_name: document.getElementById('editWorkerName').value.trim(),
- job_type: document.getElementById('editJobType').value || null,
- department_id: document.getElementById('editWorkerDept').value ? parseInt(document.getElementById('editWorkerDept').value) : null,
- phone_number: document.getElementById('editWorkerPhone').value.trim() || null,
- hire_date: document.getElementById('editWorkerHireDate').value || null,
- notes: document.getElementById('editWorkerNotes').value.trim() || null,
- status: document.getElementById('editWorkerStatus').value,
- employment_status: document.getElementById('editEmploymentStatus').value
- })});
- showToast('수정되었습니다.'); closeWorkerModal(); await loadWorkers();
- } catch(e) { showToast(e.message, 'error'); }
- });
-
- async function deactivateWorker(id, name) {
- if (!confirm(`"${name}" 작업자를 비활성화?`)) return;
- try { await api(`/workers/${id}`, { method: 'DELETE' }); showToast('작업자 비활성화 완료'); await loadWorkers(); } catch(e) { showToast(e.message, 'error'); }
- }
-
/* ===== Departments CRUD ===== */
let departments = [], departmentsLoaded = false;
@@ -2295,6 +2250,194 @@
try { await api(`/departments/${id}`, { method: 'DELETE' }); showToast('부서 비활성화 완료'); await loadDepartments(); } catch(e) { showToast(e.message, 'error'); }
}
+ /* ===== Issue Types CRUD ===== */
+ let issueCategories = [], issueItems = [], issueTypesLoaded = false;
+ let currentIssueType = 'nonconformity';
+
+ function severityBadge(severity) {
+ const colors = { low: 'bg-gray-100 text-gray-600', medium: 'bg-yellow-100 text-yellow-700', high: 'bg-orange-100 text-orange-700', critical: 'bg-red-100 text-red-700' };
+ const labels = { low: '낮음', medium: '보통', high: '높음', critical: '심각' };
+ return `
${labels[severity]||severity}`;
+ }
+
+ async function loadIssueTypes() {
+ try {
+ const [catRes, itemRes] = await Promise.all([
+ api('/work-issues/categories'),
+ api('/work-issues/items')
+ ]);
+ issueCategories = catRes.data || catRes;
+ issueItems = itemRes.data || itemRes;
+ issueTypesLoaded = true;
+ populateIssueCategorySelect();
+ displayIssueCategories();
+ } catch (err) {
+ document.getElementById('issueCategoryList').innerHTML = `
`;
+ }
+ }
+
+ function switchIssueType(type) {
+ currentIssueType = type;
+ ['nonconformity','safety','facility'].forEach(t => {
+ const btn = document.getElementById('issueTypeToggle' + t.charAt(0).toUpperCase() + t.slice(1));
+ if (btn) btn.className = 'px-3 py-1 rounded-md text-xs font-medium ' + (type === t ? 'bg-slate-700 text-white' : 'text-gray-500 hover:bg-gray-200');
+ });
+ displayIssueCategories();
+ }
+
+ function populateIssueCategorySelect() {
+ ['newIssueItemCategory', 'editIssueItemCategory'].forEach(id => {
+ const sel = document.getElementById(id); if (!sel) return;
+ const val = sel.value;
+ sel.innerHTML = '
';
+ issueCategories.filter(c => c.is_active !== 0 && c.is_active !== false).sort((a,b) => (a.display_order||0) - (b.display_order||0)).forEach(c => {
+ const typeLabel = c.category_type === 'nonconformity' ? '[부적합]' : c.category_type === 'safety' ? '[안전]' : '[시설설비]';
+ const o = document.createElement('option'); o.value = c.category_id; o.textContent = `${typeLabel} ${c.category_name}`; sel.appendChild(o);
+ });
+ sel.value = val;
+ });
+ }
+
+ function displayIssueCategories() {
+ const c = document.getElementById('issueCategoryList');
+ const filtered = issueCategories.filter(cat => cat.category_type === currentIssueType).sort((a,b) => (a.display_order||0) - (b.display_order||0));
+ if (!filtered.length) { c.innerHTML = '
등록된 카테고리가 없습니다.
'; return; }
+ c.innerHTML = filtered.map(cat => {
+ const items = issueItems.filter(it => it.category_id === cat.category_id).sort((a,b) => (a.display_order||0) - (b.display_order||0));
+ const isInactive = cat.is_active === 0 || cat.is_active === false;
+ return `
+
+
+
+ ${items.length ? items.map(it => {
+ const itemInactive = it.is_active === 0 || it.is_active === false;
+ return `
+
+
+ ├
+ ${it.item_name}
+ ${severityBadge(it.severity)}
+ ${itemInactive ? '비활성' : ''}
+
+
+
+
+
+
`;
+ }).join('') : '
아이템 없음
'}
+
+
`;
+ }).join('');
+ }
+
+ document.getElementById('addIssueCategoryForm').addEventListener('submit', async e => {
+ e.preventDefault();
+ const type = document.querySelector('input[name="newIssueCatType"]:checked').value;
+ try {
+ await api('/work-issues/categories', { method: 'POST', body: JSON.stringify({
+ category_name: document.getElementById('newIssueCatName').value.trim(),
+ category_type: type,
+ description: document.getElementById('newIssueCatDesc').value.trim() || null,
+ display_order: parseInt(document.getElementById('newIssueCatOrder').value) || 0
+ })});
+ showToast('카테고리가 추가되었습니다.');
+ document.getElementById('addIssueCategoryForm').reset();
+ await loadIssueTypes();
+ } catch(e) { showToast(e.message, 'error'); }
+ });
+
+ document.getElementById('addIssueItemForm').addEventListener('submit', async e => {
+ e.preventDefault();
+ try {
+ await api('/work-issues/items', { method: 'POST', body: JSON.stringify({
+ category_id: parseInt(document.getElementById('newIssueItemCategory').value),
+ item_name: document.getElementById('newIssueItemName').value.trim(),
+ description: document.getElementById('newIssueItemDesc').value.trim() || null,
+ severity: document.getElementById('newIssueItemSeverity').value,
+ display_order: parseInt(document.getElementById('newIssueItemOrder').value) || 0
+ })});
+ showToast('아이템이 추가되었습니다.');
+ document.getElementById('addIssueItemForm').reset();
+ await loadIssueTypes();
+ } catch(e) { showToast(e.message, 'error'); }
+ });
+
+ function editIssueCategory(id) {
+ const cat = issueCategories.find(c => c.category_id === id); if (!cat) return;
+ document.getElementById('editIssueCatId').value = cat.category_id;
+ document.getElementById('editIssueCatName').value = cat.category_name;
+ document.getElementById('editIssueCatDesc').value = cat.description || '';
+ document.getElementById('editIssueCatOrder').value = cat.display_order || 0;
+ document.getElementById('editIssueCatActive').value = (cat.is_active === 0 || cat.is_active === false) ? '0' : '1';
+ document.getElementById('editIssueCategoryModal').classList.remove('hidden');
+ }
+ function closeIssueCategoryModal() { document.getElementById('editIssueCategoryModal').classList.add('hidden'); }
+
+ document.getElementById('editIssueCategoryForm').addEventListener('submit', async e => {
+ e.preventDefault();
+ try {
+ await api(`/work-issues/categories/${document.getElementById('editIssueCatId').value}`, { method: 'PUT', body: JSON.stringify({
+ category_name: document.getElementById('editIssueCatName').value.trim(),
+ description: document.getElementById('editIssueCatDesc').value.trim() || null,
+ display_order: parseInt(document.getElementById('editIssueCatOrder').value) || 0,
+ is_active: document.getElementById('editIssueCatActive').value === '1'
+ })});
+ showToast('카테고리가 수정되었습니다.'); closeIssueCategoryModal(); await loadIssueTypes();
+ } catch(e) { showToast(e.message, 'error'); }
+ });
+
+ async function deleteIssueCategory(id, name) {
+ const items = issueItems.filter(it => it.category_id === id);
+ if (items.length > 0) { showToast(`"${name}" 카테고리에 ${items.length}개 아이템이 있어 삭제할 수 없습니다. 아이템을 먼저 삭제하세요.`, 'error'); return; }
+ if (!confirm(`"${name}" 카테고리를 삭제하시겠습니까?`)) return;
+ try { await api(`/work-issues/categories/${id}`, { method: 'DELETE' }); showToast('카테고리가 삭제되었습니다.'); await loadIssueTypes(); } catch(e) { showToast(e.message, 'error'); }
+ }
+
+ function editIssueItem(id) {
+ const it = issueItems.find(x => x.item_id === id); if (!it) return;
+ populateIssueCategorySelect();
+ document.getElementById('editIssueItemId').value = it.item_id;
+ document.getElementById('editIssueItemCategory').value = it.category_id;
+ document.getElementById('editIssueItemName').value = it.item_name;
+ document.getElementById('editIssueItemDesc').value = it.description || '';
+ document.getElementById('editIssueItemSeverity').value = it.severity || 'medium';
+ document.getElementById('editIssueItemOrder').value = it.display_order || 0;
+ document.getElementById('editIssueItemActive').value = (it.is_active === 0 || it.is_active === false) ? '0' : '1';
+ document.getElementById('editIssueItemModal').classList.remove('hidden');
+ }
+ function closeIssueItemModal() { document.getElementById('editIssueItemModal').classList.add('hidden'); }
+
+ document.getElementById('editIssueItemForm').addEventListener('submit', async e => {
+ e.preventDefault();
+ try {
+ await api(`/work-issues/items/${document.getElementById('editIssueItemId').value}`, { method: 'PUT', body: JSON.stringify({
+ category_id: parseInt(document.getElementById('editIssueItemCategory').value),
+ item_name: document.getElementById('editIssueItemName').value.trim(),
+ description: document.getElementById('editIssueItemDesc').value.trim() || null,
+ severity: document.getElementById('editIssueItemSeverity').value,
+ display_order: parseInt(document.getElementById('editIssueItemOrder').value) || 0,
+ is_active: document.getElementById('editIssueItemActive').value === '1'
+ })});
+ showToast('아이템이 수정되었습니다.'); closeIssueItemModal(); await loadIssueTypes();
+ } catch(e) { showToast(e.message, 'error'); }
+ });
+
+ async function deleteIssueItem(id, name) {
+ if (!confirm(`"${name}" 아이템을 삭제하시겠습니까?`)) return;
+ try { await api(`/work-issues/items/${id}`, { method: 'DELETE' }); showToast('아이템이 삭제되었습니다.'); await loadIssueTypes(); } catch(e) { showToast(e.message, 'error'); }
+ }
+
/* ===== Workplaces CRUD ===== */
let workplaces = [], workplacesLoaded = false, workplaceCategories = [];
let selectedWorkplaceId = null, selectedWorkplaceName = '';
diff --git a/user-management/web/nginx.conf b/user-management/web/nginx.conf
index 528d695..3da14c9 100644
--- a/user-management/web/nginx.conf
+++ b/user-management/web/nginx.conf
@@ -18,6 +18,16 @@ server {
proxy_set_header X-Real-IP $remote_addr;
}
+ # work-issues API 프록시 → system2-api
+ location /api/work-issues/ {
+ proxy_pass http://tk-system2-api:3005;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ }
+
# API 프록시 → tkuser-api
location /api/ {
proxy_pass http://tkuser-api:3000;