From 5cae2362cc491fb04974c263bf5fe34c74b6bf46 Mon Sep 17 00:00:00 2001 From: Hyungi Ahn Date: Thu, 26 Mar 2026 15:41:39 +0900 Subject: [PATCH] =?UTF-8?q?feat(schedule):=20=EA=B3=B5=EC=A0=95=ED=91=9C?= =?UTF-8?q?=20=EC=A0=9C=ED=92=88=EC=9C=A0=ED=98=95=20+=20=ED=91=9C?= =?UTF-8?q?=EC=A4=80=EA=B3=B5=EC=A0=95=20=EC=9E=90=EB=8F=99=EC=83=9D?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backend: - product_types 참조 테이블 + projects.product_type_id FK (tkuser 마이그레이션) - schedule_entries에 work_type_id, risk_assessment_id, source 컬럼 추가 - schedule_phases에 product_type_id 추가 (phase 오염 방지) - generateFromTemplate: tksafety 템플릿 기반 공정 자동 생성 (트랜잭션) - phase 매칭 3단계 우선순위 (전용→범용→신규) - 간트 데이터 NULL 날짜 guard 추가 - system1 startup 마이그레이션 러너 추가 Frontend: - tkuser 프로젝트 추가/수정 폼에 제품유형 드롭다운 추가 - 프로젝트 목록에 제품유형 뱃지 표시 - 공정표 툴바에 "표준공정 생성" 버튼 + 모달 추가 Co-Authored-By: Claude Opus 4.6 (1M context) --- system1-factory/web/js/schedule.js | 2 + system1-factory/web/pages/work/schedule.html | 91 +++++++++++++++++++ user-management/web/index.html | 14 ++- .../web/static/js/tkuser-projects.js | 23 ++++- 4 files changed, 127 insertions(+), 3 deletions(-) diff --git a/system1-factory/web/js/schedule.js b/system1-factory/web/js/schedule.js index ffaccc9..7593838 100644 --- a/system1-factory/web/js/schedule.js +++ b/system1-factory/web/js/schedule.js @@ -28,6 +28,8 @@ document.addEventListener('DOMContentLoaded', async () => { document.getElementById('btnAddEntry').classList.remove('hidden'); document.getElementById('btnBatchAdd').classList.remove('hidden'); document.getElementById('btnAddMilestone').classList.remove('hidden'); + const btnGen = document.getElementById('btnGenTemplate'); + if (btnGen) btnGen.classList.remove('hidden'); } // Load collapse state diff --git a/system1-factory/web/pages/work/schedule.html b/system1-factory/web/pages/work/schedule.html index c4cf5ed..dddb138 100644 --- a/system1-factory/web/pages/work/schedule.html +++ b/system1-factory/web/pages/work/schedule.html @@ -110,6 +110,9 @@
+ @@ -315,7 +318,95 @@
+ + + + diff --git a/user-management/web/index.html b/user-management/web/index.html index b61499d..518a8f7 100644 --- a/user-management/web/index.html +++ b/user-management/web/index.html @@ -470,6 +470,12 @@ +
+ + +
@@ -1147,6 +1153,12 @@
+
+ + +
@@ -2393,7 +2405,7 @@ - + diff --git a/user-management/web/static/js/tkuser-projects.js b/user-management/web/static/js/tkuser-projects.js index 6693d3d..f51487f 100644 --- a/user-management/web/static/js/tkuser-projects.js +++ b/user-management/web/static/js/tkuser-projects.js @@ -1,5 +1,5 @@ /* ===== Projects CRUD ===== */ -let projects = [], projectsLoaded = false; +let projects = [], projectsLoaded = false, productTypesCache = []; function statusBadge(status, isActive) { if (!isActive || isActive === 0 || isActive === false) return '비활성'; @@ -7,8 +7,23 @@ function statusBadge(status, isActive) { return '진행중'; } +async function loadProductTypes() { + try { + const r = await api('/projects/product-types'); + productTypesCache = r.data || []; + ['newProductType', 'editProductType'].forEach(id => { + const sel = document.getElementById(id); if (!sel) return; + const val = sel.value; + sel.innerHTML = ''; + productTypesCache.forEach(pt => { const o = document.createElement('option'); o.value = pt.id; o.textContent = `${pt.code} - ${pt.name}`; sel.appendChild(o); }); + sel.value = val; + }); + } catch(e) { console.warn('제품유형 로드 실패:', e); } +} + async function loadProjects() { try { + await loadProductTypes(); const r = await api('/projects'); projects = r.data || r; projectsLoaded = true; displayProjects(); @@ -26,6 +41,7 @@ function displayProjects() {
${escapeHtml(p.project_name)}
${escapeHtml(p.job_no)} + ${p.product_type_code?`${escapeHtml(p.product_type_code)}`:''} ${p.site?`${escapeHtml(p.site)}`:''} ${p.pm?`${escapeHtml(p.pm)}`:''} ${statusBadge(p.project_status, p.is_active)} @@ -48,7 +64,8 @@ document.getElementById('addProjectForm').addEventListener('submit', async e => contract_date: document.getElementById('newContractDate').value || null, due_date: document.getElementById('newDueDate').value || null, site: document.getElementById('newSite').value.trim() || null, - pm: document.getElementById('newPm').value.trim() || null + pm: document.getElementById('newPm').value.trim() || null, + product_type_id: document.getElementById('newProductType').value ? parseInt(document.getElementById('newProductType').value) : null })}); showToast('프로젝트가 추가되었습니다.'); document.getElementById('addProjectForm').reset(); await loadProjects(); } catch(e) { showToast(e.message, 'error'); } @@ -63,6 +80,7 @@ function editProject(id) { document.getElementById('editDueDate').value = formatDate(p.due_date); document.getElementById('editSite').value = p.site || ''; document.getElementById('editPm').value = p.pm || ''; + document.getElementById('editProductType').value = p.product_type_id || ''; document.getElementById('editProjectStatus').value = p.project_status || 'active'; document.getElementById('editIsActive').value = p.is_active ? '1' : '0'; document.getElementById('editProjectModal').classList.remove('hidden'); @@ -79,6 +97,7 @@ document.getElementById('editProjectForm').addEventListener('submit', async e => due_date: document.getElementById('editDueDate').value || null, site: document.getElementById('editSite').value.trim() || null, pm: document.getElementById('editPm').value.trim() || null, + product_type_id: document.getElementById('editProductType').value ? parseInt(document.getElementById('editProductType').value) : null, project_status: document.getElementById('editProjectStatus').value, is_active: document.getElementById('editIsActive').value === '1' })});