security: 보안 강제 시스템 구축 + 하드코딩 비밀번호 제거
보안 감사 결과 CRITICAL 2건, HIGH 5건 발견 → 수정 완료 + 자동화 구축. [보안 수정] - issue-view.js: 하드코딩 비밀번호 → crypto.getRandomValues() 랜덤 생성 - pushSubscriptionController.js: ntfy 비밀번호 → process.env.NTFY_SUB_PASSWORD - DEPLOY-GUIDE.md/PROGRESS.md/migration SQL: 평문 비밀번호 → placeholder - docker-compose.yml/.env.example: NTFY_SUB_PASSWORD 환경변수 추가 [보안 강제 시스템 - 신규] - scripts/security-scan.sh: 8개 규칙 (CRITICAL 2, HIGH 4, MEDIUM 2) 3모드(staged/all/diff), severity, .securityignore, MEDIUM 임계값 - .githooks/pre-commit: 로컬 빠른 피드백 - .githooks/pre-receive-server.sh: Gitea 서버 최종 차단 bypass 거버넌스([SECURITY-BYPASS: 사유] + 사용자 제한 + 로그) - SECURITY-CHECKLIST.md: 10개 카테고리 자동/수동 구분 - docs/SECURITY-GUIDE.md: 운영자 가이드 (워크플로우, bypass, FAQ) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
262
system1-factory/web/public/js/proxy-input.js
Normal file
262
system1-factory/web/public/js/proxy-input.js
Normal file
@@ -0,0 +1,262 @@
|
||||
/**
|
||||
* proxy-input.js — 대리입력 리뉴얼
|
||||
* Step 1: 날짜 선택 → 작업자 목록 (체크박스)
|
||||
* Step 2: 공통 입력 1개 → 선택된 전원 일괄 적용
|
||||
*/
|
||||
|
||||
let currentDate = '';
|
||||
let allWorkers = [];
|
||||
let selectedIds = new Set();
|
||||
let projects = [];
|
||||
let workTypes = [];
|
||||
let defectCategories = []; // { category_id, category_name, items: [{ item_id, item_name }] }
|
||||
|
||||
// ===== Init =====
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
currentDate = new Date().toISOString().substring(0, 10);
|
||||
document.getElementById('dateInput').value = currentDate;
|
||||
setTimeout(async () => {
|
||||
await loadDropdownData();
|
||||
await loadWorkers();
|
||||
}, 500);
|
||||
});
|
||||
|
||||
async function loadDropdownData() {
|
||||
try {
|
||||
const [pRes, wRes] = await Promise.all([
|
||||
window.apiCall('/projects'),
|
||||
window.apiCall('/daily-work-reports/work-types')
|
||||
]);
|
||||
projects = (pRes.data || pRes || []).filter(p => p.is_active !== 0);
|
||||
workTypes = (wRes.data || wRes || []).map(w => ({ id: w.id || w.work_type_id, name: w.name || w.work_type_name, ...w }));
|
||||
|
||||
// 부적합 대분류/소분류 로드
|
||||
const cRes = await window.apiCall('/work-issues/categories/type/nonconformity');
|
||||
const cats = cRes.data || cRes || [];
|
||||
for (const c of cats) {
|
||||
const iRes = await window.apiCall('/work-issues/items/category/' + c.category_id);
|
||||
defectCategories.push({
|
||||
category_id: c.category_id,
|
||||
category_name: c.category_name,
|
||||
items: (iRes.data || iRes || [])
|
||||
});
|
||||
}
|
||||
} catch (e) { console.warn('드롭다운 로드 실패:', e); }
|
||||
}
|
||||
|
||||
// ===== Step 1: Worker List =====
|
||||
async function loadWorkers() {
|
||||
currentDate = document.getElementById('dateInput').value;
|
||||
if (!currentDate) return;
|
||||
|
||||
const list = document.getElementById('workerList');
|
||||
list.innerHTML = '<div class="pi-skeleton"></div><div class="pi-skeleton"></div>';
|
||||
selectedIds.clear();
|
||||
updateEditButton();
|
||||
|
||||
try {
|
||||
const res = await window.apiCall('/proxy-input/daily-status?date=' + currentDate);
|
||||
if (!res.success) throw new Error(res.message);
|
||||
|
||||
allWorkers = res.data.workers || [];
|
||||
const s = res.data.summary || {};
|
||||
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;
|
||||
|
||||
renderWorkerList();
|
||||
} catch (e) {
|
||||
list.innerHTML = '<div class="pi-empty"><i class="fas fa-exclamation-triangle text-2xl text-red-300"></i><p>데이터 로드 실패</p></div>';
|
||||
}
|
||||
}
|
||||
|
||||
function renderWorkerList() {
|
||||
const list = document.getElementById('workerList');
|
||||
if (!allWorkers.length) {
|
||||
list.innerHTML = '<div class="pi-empty"><p>작업자가 없습니다</p></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// 부서별 그룹핑
|
||||
const byDept = {};
|
||||
allWorkers.forEach(w => {
|
||||
const dept = w.department_name || '미배정';
|
||||
if (!byDept[dept]) byDept[dept] = [];
|
||||
byDept[dept].push(w);
|
||||
});
|
||||
|
||||
let html = '';
|
||||
Object.keys(byDept).sort().forEach(dept => {
|
||||
html += `<div class="pi-dept-label">${esc(dept)}</div>`;
|
||||
byDept[dept].forEach(w => {
|
||||
const isFullVac = w.vacation_type_code === 'ANNUAL_FULL';
|
||||
const hasVac = !!w.vacation_type_code;
|
||||
const vacBadge = isFullVac ? '<span class="pi-badge vac">연차</span>'
|
||||
: hasVac ? `<span class="pi-badge vac-half">${esc(w.vacation_type_name)}</span>` : '';
|
||||
const doneBadge = w.has_report ? `<span class="pi-badge done">${w.total_report_hours}h</span>` : '<span class="pi-badge missing">미입력</span>';
|
||||
|
||||
html += `
|
||||
<label class="pi-worker ${isFullVac ? 'disabled' : ''}">
|
||||
<input type="checkbox" class="pi-check" value="${w.user_id}"
|
||||
${isFullVac ? 'disabled' : ''}
|
||||
onchange="onWorkerCheck(${w.user_id}, this.checked)">
|
||||
<div class="pi-worker-info">
|
||||
<span class="pi-worker-name">${esc(w.worker_name)}</span>
|
||||
<span class="pi-worker-job">${esc(w.job_type || '')}</span>
|
||||
</div>
|
||||
<div class="pi-worker-badges">${vacBadge}${doneBadge}</div>
|
||||
</label>`;
|
||||
});
|
||||
});
|
||||
list.innerHTML = html;
|
||||
}
|
||||
|
||||
function onWorkerCheck(userId, checked) {
|
||||
if (checked) selectedIds.add(userId);
|
||||
else selectedIds.delete(userId);
|
||||
updateEditButton();
|
||||
}
|
||||
|
||||
function toggleSelectAll(checked) {
|
||||
allWorkers.forEach(w => {
|
||||
if (w.vacation_type_code === 'ANNUAL_FULL') return;
|
||||
const cb = document.querySelector(`.pi-check[value="${w.user_id}"]`);
|
||||
if (cb) { cb.checked = checked; onWorkerCheck(w.user_id, checked); }
|
||||
});
|
||||
}
|
||||
|
||||
function updateEditButton() {
|
||||
const btn = document.getElementById('editBtn');
|
||||
const n = selectedIds.size;
|
||||
btn.disabled = n === 0;
|
||||
document.getElementById('editBtnText').textContent = n > 0 ? `선택 작업자 편집 (${n}명)` : '작업자를 선택하세요';
|
||||
}
|
||||
|
||||
// ===== Step 2: Bulk Edit (공통 입력 1개) =====
|
||||
function openEditMode() {
|
||||
if (selectedIds.size === 0) return;
|
||||
|
||||
const selected = allWorkers.filter(w => selectedIds.has(w.user_id));
|
||||
document.getElementById('editTitle').textContent = `일괄 편집 (${selected.length}명)`;
|
||||
|
||||
// 프로젝트/공종 드롭다운 채우기
|
||||
const projSel = document.getElementById('bulkProject');
|
||||
projSel.innerHTML = '<option value="">프로젝트 선택 *</option>' + projects.map(p => `<option value="${p.project_id}">${esc(p.project_name)}</option>`).join('');
|
||||
|
||||
const typeSel = document.getElementById('bulkWorkType');
|
||||
typeSel.innerHTML = '<option value="">공종 선택 *</option>' + workTypes.map(t => `<option value="${t.id}">${esc(t.name)}</option>`).join('');
|
||||
|
||||
// 적용 대상 목록
|
||||
document.getElementById('targetWorkers').innerHTML = selected.map(w =>
|
||||
`<span class="pi-target-chip">${esc(w.worker_name)}</span>`
|
||||
).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');
|
||||
}
|
||||
|
||||
function closeEditMode() {
|
||||
document.getElementById('step2').classList.add('hidden');
|
||||
document.getElementById('step1').classList.remove('hidden');
|
||||
}
|
||||
|
||||
// ===== 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 defectCategoryId = defect > 0 ? (parseInt(document.getElementById('bulkDefectCategory').value) || null) : null;
|
||||
const defectItemId = defect > 0 ? (parseInt(document.getElementById('bulkDefectItem').value) || null) : null;
|
||||
if (defect > 0 && !defectCategoryId) {
|
||||
showToast('부적합 대분류를 선택하세요', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const btn = document.getElementById('saveBtn');
|
||||
btn.disabled = true;
|
||||
document.getElementById('saveBtnText').textContent = '저장 중...';
|
||||
|
||||
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,
|
||||
defect_category_id: defectCategoryId,
|
||||
defect_item_id: defectItemId,
|
||||
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', {
|
||||
session_date: currentDate,
|
||||
entries
|
||||
});
|
||||
|
||||
if (res.success) {
|
||||
showToast(res.message || `${entries.length}명 저장 완료`, 'success');
|
||||
closeEditMode();
|
||||
selectedIds.clear();
|
||||
updateEditButton();
|
||||
await loadWorkers();
|
||||
} else {
|
||||
showToast(res.message || '저장 실패', 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
showToast('저장 실패: ' + (e.message || e), 'error');
|
||||
}
|
||||
|
||||
btn.disabled = false;
|
||||
document.getElementById('saveBtnText').textContent = '전체 저장';
|
||||
}
|
||||
|
||||
// ===== Defect Category/Item =====
|
||||
function onDefectChange() {
|
||||
const val = parseFloat(document.getElementById('bulkDefect').value) || 0;
|
||||
const row = document.getElementById('defectCategoryRow');
|
||||
if (val > 0) {
|
||||
row.classList.remove('hidden');
|
||||
const catSel = document.getElementById('bulkDefectCategory');
|
||||
if (catSel.options.length <= 1) {
|
||||
catSel.innerHTML = '<option value="">부적합 대분류 *</option>' +
|
||||
defectCategories.map(c => `<option value="${c.category_id}">${esc(c.category_name)}</option>`).join('');
|
||||
}
|
||||
} else {
|
||||
row.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function onDefectCategoryChange() {
|
||||
const catId = parseInt(document.getElementById('bulkDefectCategory').value);
|
||||
const itemSel = document.getElementById('bulkDefectItem');
|
||||
const cat = defectCategories.find(c => c.category_id === catId);
|
||||
itemSel.innerHTML = '<option value="">소분류 *</option>' +
|
||||
(cat ? cat.items.map(i => `<option value="${i.item_id}">${esc(i.item_name)}</option>`).join('') : '');
|
||||
}
|
||||
|
||||
function esc(s) { return (s || '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); }
|
||||
Reference in New Issue
Block a user