feat: 작업자 관리 개선 - 작업보고서 표시/현장직 구분/퇴사 처리
작업자 관리 페이지에 3가지 상태 관리 기능을 추가했습니다: 1. 작업 보고서 표시 여부 (관리자 등은 작업보고서에 표시 안함) 2. 현장직/사무직 구분 (사무직은 출퇴근 관리 불필요) 3. 퇴사 처리 (퇴사자 별도 표시) ## 주요 변경사항 ### 데이터베이스 - **마이그레이션**: 20260119095549_add_worker_display_fields.js - workers 테이블에 show_in_work_reports (BOOLEAN) 추가 - workers 테이블에 employment_status (ENUM: employed, resigned) 추가 ### 백엔드 - **workerModel.js**: create, update 함수에 새로운 필드 처리 로직 추가 ### 프론트엔드 - **worker-management.html**: 작업자 모달에 3가지 체크박스 추가 - 작업 보고서에 표시 - 현장직 (활성화) - 사무직과 구분 - 퇴사 처리 - **worker-management.js**: - 퇴사자 카드 렌더링 시 별도 스타일 적용 - 새 필드 값 로드 및 저장 처리 - **daily-work-report.js**: - 작업 보고서 작성 시 show_in_work_reports=true이고 퇴사하지 않은 작업자만 표시 ## 배포 절차 ```bash cd api.hyungi.net npm run db:migrate ``` 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,34 @@
|
|||||||
|
/**
|
||||||
|
* @param { import("knex").Knex } knex
|
||||||
|
* @returns { Promise<void> }
|
||||||
|
*/
|
||||||
|
exports.up = async function(knex) {
|
||||||
|
await knex.schema.alterTable('workers', (table) => {
|
||||||
|
// 작업 보고서 표시 여부 (기본값: true, 작업자는 표시, 관리자는 선택 가능)
|
||||||
|
table.boolean('show_in_work_reports')
|
||||||
|
.defaultTo(true)
|
||||||
|
.notNullable()
|
||||||
|
.comment('작업 보고서에 표시 여부');
|
||||||
|
|
||||||
|
// 재직 상태 (employed: 재직, resigned: 퇴사)
|
||||||
|
table.enum('employment_status', ['employed', 'resigned'])
|
||||||
|
.defaultTo('employed')
|
||||||
|
.notNullable()
|
||||||
|
.comment('재직 상태');
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('✅ workers 테이블에 show_in_work_reports, employment_status 컬럼 추가 완료');
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param { import("knex").Knex } knex
|
||||||
|
* @returns { Promise<void> }
|
||||||
|
*/
|
||||||
|
exports.down = async function(knex) {
|
||||||
|
await knex.schema.alterTable('workers', (table) => {
|
||||||
|
table.dropColumn('show_in_work_reports');
|
||||||
|
table.dropColumn('employment_status');
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('✅ workers 테이블에서 show_in_work_reports, employment_status 컬럼 삭제 완료');
|
||||||
|
};
|
||||||
@@ -19,14 +19,16 @@ const create = async (worker, callback) => {
|
|||||||
join_date = null,
|
join_date = null,
|
||||||
salary = null,
|
salary = null,
|
||||||
annual_leave = null,
|
annual_leave = null,
|
||||||
status = 'active'
|
status = 'active',
|
||||||
|
show_in_work_reports = true,
|
||||||
|
employment_status = 'employed'
|
||||||
} = worker;
|
} = worker;
|
||||||
|
|
||||||
const [result] = await db.query(
|
const [result] = await db.query(
|
||||||
`INSERT INTO workers
|
`INSERT INTO workers
|
||||||
(worker_name, job_type, join_date, salary, annual_leave, status)
|
(worker_name, job_type, join_date, salary, annual_leave, status, show_in_work_reports, employment_status)
|
||||||
VALUES (?, ?, ?, ?, ?, ?)`,
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
[worker_name, job_type, formatDate(join_date), salary, annual_leave, status]
|
[worker_name, job_type, formatDate(join_date), salary, annual_leave, status, show_in_work_reports, employment_status]
|
||||||
);
|
);
|
||||||
|
|
||||||
callback(null, result.insertId);
|
callback(null, result.insertId);
|
||||||
@@ -81,7 +83,9 @@ const update = async (worker, callback) => {
|
|||||||
status,
|
status,
|
||||||
join_date,
|
join_date,
|
||||||
salary,
|
salary,
|
||||||
annual_leave
|
annual_leave,
|
||||||
|
show_in_work_reports,
|
||||||
|
employment_status
|
||||||
} = worker;
|
} = worker;
|
||||||
|
|
||||||
// 업데이트할 필드만 동적으로 구성
|
// 업데이트할 필드만 동적으로 구성
|
||||||
@@ -112,6 +116,14 @@ const update = async (worker, callback) => {
|
|||||||
updates.push('annual_leave = ?');
|
updates.push('annual_leave = ?');
|
||||||
values.push(annual_leave);
|
values.push(annual_leave);
|
||||||
}
|
}
|
||||||
|
if (show_in_work_reports !== undefined) {
|
||||||
|
updates.push('show_in_work_reports = ?');
|
||||||
|
values.push(show_in_work_reports);
|
||||||
|
}
|
||||||
|
if (employment_status !== undefined) {
|
||||||
|
updates.push('employment_status = ?');
|
||||||
|
values.push(employment_status);
|
||||||
|
}
|
||||||
|
|
||||||
if (updates.length === 0) {
|
if (updates.length === 0) {
|
||||||
callback(new Error('업데이트할 필드가 없습니다'));
|
callback(new Error('업데이트할 필드가 없습니다'));
|
||||||
|
|||||||
@@ -206,16 +206,21 @@ async function loadData() {
|
|||||||
async function loadWorkers() {
|
async function loadWorkers() {
|
||||||
try {
|
try {
|
||||||
console.log('Workers API 호출 중... (통합 API 사용)');
|
console.log('Workers API 호출 중... (통합 API 사용)');
|
||||||
// 활성 작업자 1000명까지 조회 (서버 사이드 필터링 적용)
|
// 모든 작업자 1000명까지 조회
|
||||||
const data = await window.apiCall(`${window.API}/workers?status=active&limit=1000`);
|
const data = await window.apiCall(`${window.API}/workers?limit=1000`);
|
||||||
const allWorkers = Array.isArray(data) ? data : (data.data || data.workers || []);
|
const allWorkers = Array.isArray(data) ? data : (data.data || data.workers || []);
|
||||||
|
|
||||||
// 활성화된 작업자만 필터링
|
// 작업 보고서에 표시할 작업자만 필터링
|
||||||
|
// 1. show_in_work_reports가 true
|
||||||
|
// 2. employment_status가 resigned가 아님
|
||||||
workers = allWorkers.filter(worker => {
|
workers = allWorkers.filter(worker => {
|
||||||
return worker.status === 'active' || worker.is_active === 1 || worker.is_active === true;
|
const showInReports = worker.show_in_work_reports !== 0 && worker.show_in_work_reports !== false;
|
||||||
|
const notResigned = worker.employment_status !== 'resigned';
|
||||||
|
return showInReports && notResigned;
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`✅ Workers 로드 성공: ${workers.length}명 (전체: ${allWorkers.length}명)`);
|
console.log(`✅ Workers 로드 성공: ${workers.length}명 (전체: ${allWorkers.length}명)`);
|
||||||
|
console.log(`📊 필터링 조건: show_in_work_reports=true, employment_status≠resigned`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('작업자 로딩 오류:', error);
|
console.error('작업자 로딩 오류:', error);
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
@@ -197,18 +197,23 @@ function renderWorkers() {
|
|||||||
|
|
||||||
const jobType = jobTypeMap[worker.job_type] || jobTypeMap['worker'];
|
const jobType = jobTypeMap[worker.job_type] || jobTypeMap['worker'];
|
||||||
const isInactive = worker.status === 'inactive' || worker.is_active === 0 || worker.is_active === false;
|
const isInactive = worker.status === 'inactive' || worker.is_active === 0 || worker.is_active === false;
|
||||||
|
const isResigned = worker.employment_status === 'resigned';
|
||||||
|
const showInWorkReports = worker.show_in_work_reports !== 0 && worker.show_in_work_reports !== false;
|
||||||
|
|
||||||
console.log('🎨 카드 렌더링:', {
|
console.log('🎨 카드 렌더링:', {
|
||||||
worker_id: worker.worker_id,
|
worker_id: worker.worker_id,
|
||||||
worker_name: worker.worker_name,
|
worker_name: worker.worker_name,
|
||||||
status: worker.status,
|
status: worker.status,
|
||||||
is_active: worker.is_active,
|
is_active: worker.is_active,
|
||||||
isInactive: isInactive
|
isInactive: isInactive,
|
||||||
|
isResigned: isResigned,
|
||||||
|
showInWorkReports: showInWorkReports
|
||||||
});
|
});
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="project-card worker-card ${isInactive ? 'inactive' : ''}" onclick="editWorker(${worker.worker_id})">
|
<div class="project-card worker-card ${isResigned ? 'resigned' : ''} ${isInactive ? 'inactive' : ''}" onclick="editWorker(${worker.worker_id})">
|
||||||
${isInactive ? '<div class="inactive-overlay"><span class="inactive-badge">🚫 비활성화됨</span></div>' : ''}
|
${isResigned ? '<div class="inactive-overlay"><span class="inactive-badge" style="background: #dc2626;">🚪 퇴사</span></div>' :
|
||||||
|
isInactive ? '<div class="inactive-overlay"><span class="inactive-badge">🏢 사무직</span></div>' : ''}
|
||||||
<div class="project-header">
|
<div class="project-header">
|
||||||
<div class="project-info">
|
<div class="project-info">
|
||||||
<div class="worker-avatar">
|
<div class="worker-avatar">
|
||||||
@@ -216,7 +221,8 @@ function renderWorkers() {
|
|||||||
</div>
|
</div>
|
||||||
<h3 class="project-name">
|
<h3 class="project-name">
|
||||||
${worker.worker_name}
|
${worker.worker_name}
|
||||||
${isInactive ? '<span class="inactive-label">(비활성)</span>' : ''}
|
${isResigned ? '<span class="inactive-label" style="color: #dc2626;">(퇴사)</span>' :
|
||||||
|
isInactive ? '<span class="inactive-label">(사무직)</span>' : ''}
|
||||||
</h3>
|
</h3>
|
||||||
<div class="project-meta">
|
<div class="project-meta">
|
||||||
<span style="color: ${jobType.color}; font-weight: 500;">${jobType.icon} ${jobType.text}</span>
|
<span style="color: ${jobType.color}; font-weight: 500;">${jobType.icon} ${jobType.text}</span>
|
||||||
@@ -224,14 +230,15 @@ function renderWorkers() {
|
|||||||
${worker.email ? `<span>📧 ${worker.email}</span>` : ''}
|
${worker.email ? `<span>📧 ${worker.email}</span>` : ''}
|
||||||
${worker.department ? `<span>🏢 ${worker.department}</span>` : ''}
|
${worker.department ? `<span>🏢 ${worker.department}</span>` : ''}
|
||||||
${worker.hire_date ? `<span>📅 입사: ${formatDate(worker.hire_date)}</span>` : ''}
|
${worker.hire_date ? `<span>📅 입사: ${formatDate(worker.hire_date)}</span>` : ''}
|
||||||
${isInactive ? '<span class="inactive-notice">⚠️ 작업보고서에서 숨김</span>' : ''}
|
${isResigned ? '<span class="inactive-notice" style="color: #dc2626;">⚠️ 퇴사 처리됨</span>' :
|
||||||
|
!showInWorkReports ? '<span class="inactive-notice">⚠️ 작업보고서에서 숨김</span>' : ''}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="project-actions">
|
<div class="project-actions">
|
||||||
<button class="btn-toggle ${isInactive ? 'btn-activate' : 'btn-deactivate'}"
|
<button class="btn-toggle ${isInactive ? 'btn-activate' : 'btn-deactivate'}"
|
||||||
onclick="event.stopPropagation(); toggleWorkerStatus(${worker.worker_id})"
|
onclick="event.stopPropagation(); toggleWorkerStatus(${worker.worker_id})"
|
||||||
title="${isInactive ? '활성화' : '비활성화'}">
|
title="${isInactive ? '현장직으로 변경' : '사무직으로 변경'}">
|
||||||
${isInactive ? '✅' : '❌'}
|
${isInactive ? '🏭' : '🏢'}
|
||||||
</button>
|
</button>
|
||||||
<button class="btn-edit" onclick="event.stopPropagation(); editWorker(${worker.worker_id})" title="수정">
|
<button class="btn-edit" onclick="event.stopPropagation(); editWorker(${worker.worker_id})" title="수정">
|
||||||
✏️
|
✏️
|
||||||
@@ -449,13 +456,24 @@ function openWorkerModal(worker = null) {
|
|||||||
const isActiveValue = worker.status !== 'inactive' && worker.is_active !== 0 && worker.is_active !== false;
|
const isActiveValue = worker.status !== 'inactive' && worker.is_active !== 0 && worker.is_active !== false;
|
||||||
document.getElementById('isActive').checked = isActiveValue;
|
document.getElementById('isActive').checked = isActiveValue;
|
||||||
|
|
||||||
|
// show_in_work_reports 값 처리
|
||||||
|
const showInWorkReportsValue = worker.show_in_work_reports !== 0 && worker.show_in_work_reports !== false;
|
||||||
|
document.getElementById('showInWorkReports').checked = showInWorkReportsValue;
|
||||||
|
|
||||||
|
// employment_status 값 처리 (resigned이면 체크)
|
||||||
|
const isResignedValue = worker.employment_status === 'resigned';
|
||||||
|
document.getElementById('isResigned').checked = isResignedValue;
|
||||||
|
|
||||||
console.log('🔧 작업자 로드:', {
|
console.log('🔧 작업자 로드:', {
|
||||||
worker_id: worker.worker_id,
|
worker_id: worker.worker_id,
|
||||||
worker_name: worker.worker_name,
|
worker_name: worker.worker_name,
|
||||||
job_type: worker.job_type,
|
job_type: worker.job_type,
|
||||||
status: worker.status,
|
status: worker.status,
|
||||||
is_active_raw: worker.is_active,
|
is_active_raw: worker.is_active,
|
||||||
is_active_processed: isActiveValue
|
is_active_processed: isActiveValue,
|
||||||
|
show_in_work_reports: showInWorkReportsValue,
|
||||||
|
employment_status: worker.employment_status,
|
||||||
|
is_resigned: isResignedValue
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// 신규 등록 모드
|
// 신규 등록 모드
|
||||||
@@ -466,6 +484,8 @@ function openWorkerModal(worker = null) {
|
|||||||
document.getElementById('workerForm').reset();
|
document.getElementById('workerForm').reset();
|
||||||
document.getElementById('workerId').value = '';
|
document.getElementById('workerId').value = '';
|
||||||
document.getElementById('isActive').checked = true;
|
document.getElementById('isActive').checked = true;
|
||||||
|
document.getElementById('showInWorkReports').checked = true;
|
||||||
|
document.getElementById('isResigned').checked = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
modal.style.display = 'flex';
|
modal.style.display = 'flex';
|
||||||
@@ -510,7 +530,9 @@ async function saveWorker() {
|
|||||||
join_date: document.getElementById('joinDate')?.value || null,
|
join_date: document.getElementById('joinDate')?.value || null,
|
||||||
salary: document.getElementById('salary')?.value || null,
|
salary: document.getElementById('salary')?.value || null,
|
||||||
annual_leave: document.getElementById('annualLeave')?.value || null,
|
annual_leave: document.getElementById('annualLeave')?.value || null,
|
||||||
status: document.getElementById('isActive').checked ? 'active' : 'inactive'
|
status: document.getElementById('isActive').checked ? 'active' : 'inactive',
|
||||||
|
show_in_work_reports: document.getElementById('showInWorkReports').checked,
|
||||||
|
employment_status: document.getElementById('isResigned').checked ? 'resigned' : 'employed'
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log('💾 저장할 작업자 데이터:', JSON.stringify(workerData, null, 2));
|
console.log('💾 저장할 작업자 데이터:', JSON.stringify(workerData, null, 2));
|
||||||
@@ -574,7 +596,9 @@ async function toggleWorkerStatus(workerId) {
|
|||||||
status: newStatus,
|
status: newStatus,
|
||||||
join_date: worker.join_date || null,
|
join_date: worker.join_date || null,
|
||||||
salary: worker.salary || null,
|
salary: worker.salary || null,
|
||||||
annual_leave: worker.annual_leave || null
|
annual_leave: worker.annual_leave || null,
|
||||||
|
show_in_work_reports: worker.show_in_work_reports !== 0 && worker.show_in_work_reports !== false,
|
||||||
|
employment_status: worker.employment_status || 'employed'
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log('📤 전송 데이터:', JSON.stringify(updateData, null, 2));
|
console.log('📤 전송 데이터:', JSON.stringify(updateData, null, 2));
|
||||||
|
|||||||
@@ -171,12 +171,38 @@
|
|||||||
<textarea id="notes" class="form-control" rows="3" placeholder="추가 정보나 특이사항을 입력하세요"></textarea>
|
<textarea id="notes" class="form-control" rows="3" placeholder="추가 정보나 특이사항을 입력하세요"></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 상태 관리 섹션 -->
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label" style="display: flex; align-items: center; gap: 0.5rem;">
|
<label class="form-label" style="font-weight: 600; margin-bottom: 0.75rem; display: block;">상태 관리</label>
|
||||||
<input type="checkbox" id="isActive" checked style="margin: 0;">
|
|
||||||
<span>작업자 활성화</span>
|
<div style="display: flex; flex-direction: column; gap: 0.75rem;">
|
||||||
</label>
|
<!-- 작업 보고서 표시 -->
|
||||||
<small style="color: #6b7280; font-size: 0.8rem;">체크 해제 시 작업보고서 입력에서 숨겨집니다</small>
|
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
|
||||||
|
<input type="checkbox" id="showInWorkReports" checked style="margin: 0; cursor: pointer;">
|
||||||
|
<span>작업 보고서에 표시</span>
|
||||||
|
</label>
|
||||||
|
<small style="color: #6b7280; font-size: 0.75rem; margin-top: -0.5rem; margin-left: 1.5rem;">
|
||||||
|
체크 해제 시 일일 작업보고서 작성 시 이 작업자가 목록에 나타나지 않습니다
|
||||||
|
</small>
|
||||||
|
|
||||||
|
<!-- 현장직/사무직 구분 -->
|
||||||
|
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
|
||||||
|
<input type="checkbox" id="isActive" checked style="margin: 0; cursor: pointer;">
|
||||||
|
<span>현장직 (활성화)</span>
|
||||||
|
</label>
|
||||||
|
<small style="color: #6b7280; font-size: 0.75rem; margin-top: -0.5rem; margin-left: 1.5rem;">
|
||||||
|
체크: 현장직 (출퇴근 관리 필요) / 체크 해제: 사무직 (출퇴근 관리 불필요)
|
||||||
|
</small>
|
||||||
|
|
||||||
|
<!-- 퇴사 처리 -->
|
||||||
|
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
|
||||||
|
<input type="checkbox" id="isResigned" style="margin: 0; cursor: pointer;">
|
||||||
|
<span style="color: #ef4444;">퇴사 처리</span>
|
||||||
|
</label>
|
||||||
|
<small style="color: #ef4444; font-size: 0.75rem; margin-top: -0.5rem; margin-left: 1.5rem;">
|
||||||
|
퇴사한 작업자로 표시됩니다. 작업자 목록에서 별도로 표시됩니다
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user