From f68c66e696ffcaa467a0ab42fbc2894925b9ecde Mon Sep 17 00:00:00 2001 From: Hyungi Ahn Date: Tue, 31 Mar 2026 14:57:30 +0900 Subject: [PATCH] =?UTF-8?q?fix(proxy-input):=20worker=5Fid=E2=86=92user=5F?= =?UTF-8?q?id=20=EC=88=98=EC=A0=95=20+=20=EA=B3=B5=ED=86=B5=20=EC=9E=85?= =?UTF-8?q?=EB=A0=A5=20UI=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 백엔드: - proxyInputModel 전체 worker_id→user_id 전환 (작업보고서/휴가 매핑 실패 → 전부 미입력으로 표시되던 문제) 프론트: - 개별 입력 → 공통 입력 1개로 전환 프로젝트/공종/시간/부적합 한번 입력 → 선택된 전원에 적용 - 부서별 그룹핑 표시 - 적용 대상 칩 표시 Co-Authored-By: Claude Opus 4.6 (1M context) --- system1-factory/api/models/proxyInputModel.js | 18 +- system1-factory/web/css/proxy-input.css | 18 ++ system1-factory/web/js/proxy-input.js | 198 +++++++----------- .../web/pages/work/proxy-input.html | 17 +- 4 files changed, 123 insertions(+), 128 deletions(-) diff --git a/system1-factory/api/models/proxyInputModel.js b/system1-factory/api/models/proxyInputModel.js index 2d739ea..d679bda 100644 --- a/system1-factory/api/models/proxyInputModel.js +++ b/system1-factory/api/models/proxyInputModel.js @@ -14,7 +14,7 @@ const ProxyInputModel = { SELECT ta.user_id, w.worker_name, ta.session_id FROM tbm_team_assignments ta JOIN tbm_sessions s ON ta.session_id = s.session_id - JOIN workers w ON ta.user_id = w.worker_id + JOIN workers w ON ta.user_id = w.user_id WHERE s.session_date = ? AND ta.user_id IN (${placeholders}) AND s.status != 'cancelled' `, [sessionDate, ...userIds]); return rows; @@ -27,9 +27,9 @@ const ProxyInputModel = { if (!userIds.length) return []; const placeholders = userIds.map(() => '?').join(','); const [rows] = await conn.query(` - SELECT worker_id FROM workers WHERE worker_id IN (${placeholders}) AND status = 'active' + SELECT user_id FROM workers WHERE user_id IN (${placeholders}) AND status = 'active' `, [...userIds]); - return rows.map(r => r.worker_id); + return rows.map(r => r.user_id); }, /** @@ -73,11 +73,11 @@ const ProxyInputModel = { // 1. 활성 작업자 const [workers] = await db.query(` - SELECT w.worker_id AS user_id, w.worker_name, w.job_type, + SELECT w.user_id, w.worker_name, w.job_type, COALESCE(d.department_name, '미배정') AS department_name FROM workers w LEFT JOIN departments d ON w.department_id = d.department_id - WHERE w.status = 'active' + WHERE w.status = 'active' AND w.user_id IS NOT NULL ORDER BY w.worker_name `); @@ -87,7 +87,7 @@ const ProxyInputModel = { lu.worker_name AS leader_name, s.is_proxy_input FROM tbm_team_assignments ta JOIN tbm_sessions s ON ta.session_id = s.session_id - LEFT JOIN workers lu ON s.leader_user_id = lu.worker_id + LEFT JOIN workers lu ON s.leader_user_id = lu.user_id WHERE s.session_date = ? AND s.status != 'cancelled' `, [date]); @@ -191,11 +191,11 @@ const ProxyInputModel = { // 작업자 정보 const [workerRows] = await db.query(` - SELECT w.worker_id AS user_id, w.worker_name, w.job_type, + SELECT w.user_id, w.worker_name, w.job_type, COALESCE(d.department_name, '미배정') AS department_name FROM workers w LEFT JOIN departments d ON w.department_id = d.department_id - WHERE w.worker_id = ? + WHERE w.user_id = ? `, [userId]); // TBM 세션 @@ -206,7 +206,7 @@ const ProxyInputModel = { p.project_name, wt.work_type_name, ta.work_hours FROM tbm_team_assignments ta JOIN tbm_sessions s ON ta.session_id = s.session_id - LEFT JOIN workers lu ON s.leader_user_id = lu.worker_id + LEFT JOIN workers lu ON s.leader_user_id = lu.user_id LEFT JOIN sso_users pu ON s.proxy_input_by = pu.user_id LEFT JOIN projects p ON ta.project_id = p.project_id LEFT JOIN work_types wt ON ta.work_type_id = wt.work_type_id diff --git a/system1-factory/web/css/proxy-input.css b/system1-factory/web/css/proxy-input.css index 29334ee..e553bc8 100644 --- a/system1-factory/web/css/proxy-input.css +++ b/system1-factory/web/css/proxy-input.css @@ -60,6 +60,24 @@ .pi-skeleton { height: 52px; border-radius: 10px; background: linear-gradient(90deg, #e5e7eb 25%, #f3f4f6 50%, #e5e7eb 75%); background-size: 200% 100%; animation: pi-shimmer 1.5s infinite; } @keyframes pi-shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } } +/* Department Label */ +.pi-dept-label { font-size: 11px; font-weight: 700; color: #6b7280; text-transform: uppercase; letter-spacing: 0.5px; padding: 8px 2px 4px; } + +/* Bulk Form */ +.pi-bulk-form { background: white; border-radius: 12px; padding: 14px; margin-bottom: 12px; box-shadow: 0 1px 4px rgba(0,0,0,0.08); display: flex; flex-direction: column; gap: 8px; } +.pi-edit-row { display: flex; gap: 8px; } +.pi-select { flex: 1; padding: 8px 10px; border: 1px solid #d1d5db; border-radius: 8px; font-size: 14px; background: white; } +.pi-field { display: flex; flex-direction: column; gap: 2px; flex: 1; } +.pi-field span { font-size: 11px; color: #6b7280; font-weight: 600; } +.pi-input { padding: 8px; border: 1px solid #d1d5db; border-radius: 8px; font-size: 15px; text-align: center; font-weight: 600; } +.pi-note-input { width: 100%; padding: 8px 10px; border: 1px solid #d1d5db; border-radius: 8px; font-size: 13px; } + +/* Target Section */ +.pi-target-section { background: white; border-radius: 12px; padding: 12px; margin-bottom: 80px; box-shadow: 0 1px 4px rgba(0,0,0,0.08); } +.pi-target-label { font-size: 12px; font-weight: 700; color: #6b7280; margin-bottom: 8px; } +.pi-target-list { display: flex; flex-wrap: wrap; gap: 6px; } +.pi-target-chip { font-size: 12px; font-weight: 600; padding: 4px 10px; border-radius: 20px; background: #dbeafe; color: #1e40af; } + /* Empty */ .pi-empty { display: flex; flex-direction: column; align-items: center; gap: 8px; padding: 48px 16px; color: #9ca3af; font-size: 14px; } diff --git a/system1-factory/web/js/proxy-input.js b/system1-factory/web/js/proxy-input.js index 81b8bf4..1508bf3 100644 --- a/system1-factory/web/js/proxy-input.js +++ b/system1-factory/web/js/proxy-input.js @@ -1,7 +1,7 @@ /** * proxy-input.js — 대리입력 리뉴얼 * Step 1: 날짜 선택 → 작업자 목록 (체크박스) - * Step 2: 선택 작업자 일괄 편집 → 저장 (TBM 자동 생성) + * Step 2: 공통 입력 1개 → 선택된 전원 일괄 적용 */ let currentDate = ''; @@ -9,14 +9,11 @@ let allWorkers = []; let selectedIds = new Set(); let projects = []; let workTypes = []; -let editData = {}; // { userId: { project_id, work_type_id, work_hours, defect_hours, note } } // ===== Init ===== document.addEventListener('DOMContentLoaded', () => { - const now = new Date(); - currentDate = now.toISOString().substring(0, 10); + currentDate = new Date().toISOString().substring(0, 10); document.getElementById('dateInput').value = currentDate; - setTimeout(async () => { await loadDropdownData(); await loadWorkers(); @@ -50,7 +47,7 @@ async function loadWorkers() { allWorkers = res.data.workers || []; const s = res.data.summary || {}; - document.getElementById('totalNum').textContent = s.total_active_workers || 0; + document.getElementById('totalNum').textContent = s.total_active_workers || allWorkers.length; document.getElementById('doneNum').textContent = s.report_completed || 0; document.getElementById('missingNum').textContent = s.report_missing || 0; document.getElementById('vacNum').textContent = allWorkers.filter(w => w.vacation_type_code === 'ANNUAL_FULL').length; @@ -68,27 +65,38 @@ function renderWorkerList() { return; } - list.innerHTML = allWorkers.map(w => { - const isFullVac = w.vacation_type_code === 'ANNUAL_FULL'; - const hasVac = !!w.vacation_type_code; - const vacBadge = isFullVac ? '연차' - : hasVac ? `${esc(w.vacation_type_name)}` : ''; - const doneBadge = w.has_report ? `${w.total_report_hours}h` : '미입력'; + // 부서별 그룹핑 + const byDept = {}; + allWorkers.forEach(w => { + const dept = w.department_name || '미배정'; + if (!byDept[dept]) byDept[dept] = []; + byDept[dept].push(w); + }); - return ` - `; - }).join(''); + let html = ''; + Object.keys(byDept).sort().forEach(dept => { + html += `
${esc(dept)}
`; + byDept[dept].forEach(w => { + const isFullVac = w.vacation_type_code === 'ANNUAL_FULL'; + const hasVac = !!w.vacation_type_code; + const vacBadge = isFullVac ? '연차' + : hasVac ? `${esc(w.vacation_type_name)}` : ''; + const doneBadge = w.has_report ? `${w.total_report_hours}h` : '미입력'; + + html += ` + `; + }); + }); + list.innerHTML = html; } function onWorkerCheck(userId, checked) { @@ -112,61 +120,29 @@ function updateEditButton() { document.getElementById('editBtnText').textContent = n > 0 ? `선택 작업자 편집 (${n}명)` : '작업자를 선택하세요'; } -// ===== Step 2: Edit Mode ===== +// ===== Step 2: Bulk Edit (공통 입력 1개) ===== function openEditMode() { if (selectedIds.size === 0) return; const selected = allWorkers.filter(w => selectedIds.has(w.user_id)); - editData = {}; - - // 기본값 설정 - selected.forEach(w => { - const isHalfVac = w.vacation_type_code === 'ANNUAL_HALF'; - const isQuarterVac = w.vacation_type_code === 'ANNUAL_QUARTER'; - editData[w.user_id] = { - project_id: '', - work_type_id: '', - work_hours: isHalfVac ? 4 : isQuarterVac ? 6 : 8, - defect_hours: 0, - note: '', - start_time: '08:00', - end_time: isHalfVac ? '12:00' : isQuarterVac ? '14:00' : '17:00', - work_status_id: 1 - }; - }); - document.getElementById('editTitle').textContent = `일괄 편집 (${selected.length}명)`; - const projOpts = projects.map(p => ``).join(''); - const typeOpts = workTypes.map(t => ``).join(''); + // 프로젝트/공종 드롭다운 채우기 + const projSel = document.getElementById('bulkProject'); + projSel.innerHTML = '' + projects.map(p => ``).join(''); - document.getElementById('editList').innerHTML = selected.map(w => { - const d = editData[w.user_id]; - const vacLabel = w.vacation_type_name ? ` ${esc(w.vacation_type_name)}` : ''; - return ` -
-
- ${esc(w.worker_name)} - ${esc(w.job_type || '')} - ${vacLabel} -
-
-
- - -
-
- - -
- -
-
`; - }).join(''); + const typeSel = document.getElementById('bulkWorkType'); + typeSel.innerHTML = '' + workTypes.map(t => ``).join(''); + + // 적용 대상 목록 + document.getElementById('targetWorkers').innerHTML = selected.map(w => + `${esc(w.worker_name)}` + ).join(''); + + // 기본값 + document.getElementById('bulkHours').value = '8'; + document.getElementById('bulkDefect').value = '0'; + document.getElementById('bulkNote').value = ''; document.getElementById('step1').classList.add('hidden'); document.getElementById('step2').classList.remove('hidden'); @@ -179,54 +155,40 @@ function closeEditMode() { // ===== Save ===== async function saveAll() { + const projId = document.getElementById('bulkProject').value; + const wtypeId = document.getElementById('bulkWorkType').value; + const hours = parseFloat(document.getElementById('bulkHours').value) || 0; + const defect = parseFloat(document.getElementById('bulkDefect').value) || 0; + const note = document.getElementById('bulkNote').value.trim(); + + if (!projId || !wtypeId) { + showToast('프로젝트와 공종을 선택하세요', 'error'); + return; + } + if (hours <= 0) { + showToast('근무시간을 입력하세요', 'error'); + return; + } + if (defect > hours) { + showToast('부적합 시간이 근무시간을 초과합니다', 'error'); + return; + } + const btn = document.getElementById('saveBtn'); btn.disabled = true; document.getElementById('saveBtnText').textContent = '저장 중...'; - const entries = []; - let hasError = false; - - for (const uid of selectedIds) { - const projEl = document.getElementById('proj_' + uid); - const wtypeEl = document.getElementById('wtype_' + uid); - const hoursEl = document.getElementById('hours_' + uid); - const defectEl = document.getElementById('defect_' + uid); - const noteEl = document.getElementById('note_' + uid); - - if (!projEl?.value || !wtypeEl?.value) { - showToast(`프로젝트/공종을 선택하세요`, 'error'); - projEl?.focus(); - hasError = true; - break; - } - - const workHours = parseFloat(hoursEl?.value) || 0; - const defectHours = parseFloat(defectEl?.value) || 0; - - if (defectHours > workHours) { - showToast('부적합 시간이 근무시간을 초과합니다', 'error'); - hasError = true; - break; - } - - entries.push({ - user_id: uid, - project_id: parseInt(projEl.value), - work_type_id: parseInt(wtypeEl.value), - work_hours: workHours, - defect_hours: defectHours, - note: noteEl?.value || '', - start_time: '08:00', - end_time: '17:00', - work_status_id: defectHours > 0 ? 2 : 1 - }); - } - - if (hasError) { - btn.disabled = false; - document.getElementById('saveBtnText').textContent = '전체 저장'; - return; - } + const entries = Array.from(selectedIds).map(uid => ({ + user_id: uid, + project_id: parseInt(projId), + work_type_id: parseInt(wtypeId), + work_hours: hours, + defect_hours: defect, + note: note, + start_time: '08:00', + end_time: '17:00', + work_status_id: defect > 0 ? 2 : 1 + })); try { const res = await window.apiCall('/proxy-input', 'POST', { diff --git a/system1-factory/web/pages/work/proxy-input.html b/system1-factory/web/pages/work/proxy-input.html index 6c6256e..94fc679 100644 --- a/system1-factory/web/pages/work/proxy-input.html +++ b/system1-factory/web/pages/work/proxy-input.html @@ -73,7 +73,22 @@

일괄 편집

-
+
+
+ + +
+
+ + +
+ +
+ +
+
적용 대상
+
+