Fix: Worker/Project status update and filtering issues

- Added cache invalidation for Workers and Projects
- Implemented server-side status filtering for Workers
- Fixed worker update query bug (removed non-existent join_date column)
- Updated daily-work-report UI to fetch only active workers
This commit is contained in:
Hyungi Ahn
2026-01-06 15:50:40 +09:00
parent 3549710325
commit 48fff7df64
6 changed files with 226 additions and 187 deletions

View File

@@ -27,6 +27,20 @@
4. **테스트 계정 생성**
- `tester` / `000000` 관리자(Leader) 계정 생성.
### 🛠️ 작업자 및 프로젝트 관리 기능 개선 (2026-01-06)
**개요**: 작업자/프로젝트의 비활성화(퇴사/종료) 처리가 즉시 반영되지 않는 문제 및 로직 오류 수정.
1. **캐시 무효화 및 필터링 적용 (Cache & Filtering)**
- **문제**: 작업자/프로젝트 상태 변경 후에도 캐시가 남아있어 드롭다운 목록에서 사라지지 않음.
- **해결**:
- `WorkerController`, `ProjectController`: 생성/수정/삭제 시 `request` 단위의 캐시 즉시 무효화 로직 추가.
- `WorkerController`: 목록 조회 시 `status` 파라미터 지원 추가.
- `daily-work-report.js`: 작업보고서 작성 시 `active` 상태인 작업자만 필터링하여 조회하도록 수정.
2. **작업자 비활성화 오류 수정 (Bug Fix)**
- **원인**: `workerModel.update` 쿼리에 DB에 존재하지 않는 `join_date` 컬럼을 업데이트하려는 시도가 있어 SQL 에러 발생.
- **해결**: `workerModel.js`에서 잘못된 컬럼(`join_date`) 참조 제거. (올바른 컬럼 `hire_date`는 유지)
---
## 🛡보안 및 검토 리포트 (History)

View File

@@ -11,6 +11,7 @@ const projectModel = require('../models/projectModel');
const { ValidationError, NotFoundError, DatabaseError } = require('../utils/errors');
const { asyncHandler } = require('../middlewares/errorHandler');
const logger = require('../utils/logger');
const cache = require('../utils/cache');
/**
* 프로젝트 생성
@@ -27,6 +28,9 @@ exports.createProject = asyncHandler(async (req, res) => {
});
});
// 프로젝트 캐시 무효화
await cache.invalidateCache.project();
logger.info('프로젝트 생성 성공', { project_id: id });
res.status(201).json({
@@ -123,6 +127,9 @@ exports.updateProject = asyncHandler(async (req, res) => {
throw new NotFoundError('프로젝트를 찾을 수 없습니다');
}
// 프로젝트 캐시 무효화
await cache.invalidateCache.project();
logger.info('프로젝트 수정 성공', { project_id: id });
res.json({
@@ -153,6 +160,9 @@ exports.removeProject = asyncHandler(async (req, res) => {
throw new NotFoundError('프로젝트를 찾을 수 없습니다');
}
// 프로젝트 캐시 무효화
await cache.invalidateCache.project();
logger.info('프로젝트 삭제 성공', { project_id: id });
res.json({

View File

@@ -45,9 +45,9 @@ exports.createWorker = asyncHandler(async (req, res) => {
* 전체 작업자 조회 (캐싱 및 페이지네이션 적용)
*/
exports.getAllWorkers = asyncHandler(async (req, res) => {
const { page = 1, limit = 10, search = '' } = req.query;
const { page = 1, limit = 10, search = '', status = '' } = req.query;
const cacheKey = cache.createKey('workers', 'list', page, limit, search);
const cacheKey = cache.createKey('workers', 'list', page, limit, search, status);
// 캐시에서 조회
const cachedData = await cache.get(cacheKey);
@@ -62,7 +62,7 @@ exports.getAllWorkers = asyncHandler(async (req, res) => {
}
// 최적화된 쿼리 사용
const result = await optimizedQueries.getWorkersPaged(page, limit, search);
const result = await optimizedQueries.getWorkersPaged(page, limit, search, status);
// 캐시에 저장 (5분)
await cache.set(cacheKey, result, cache.TTL.MEDIUM);
@@ -127,6 +127,10 @@ exports.updateWorker = asyncHandler(async (req, res) => {
throw new NotFoundError('작업자를 찾을 수 없습니다');
}
// 작업자 관련 캐시 무효화
logger.info('작업자 수정 후 캐시 무효화', { worker_id: id });
await cache.invalidateCache.worker();
logger.info('작업자 수정 성공', { worker_id: id });
res.json({

View File

@@ -79,7 +79,6 @@ const update = async (worker, callback) => {
worker_id,
worker_name,
job_type,
join_date,
status,
phone_number,
email,
@@ -92,7 +91,6 @@ const update = async (worker, callback) => {
`UPDATE workers
SET worker_name = ?,
job_type = ?,
join_date = ?,
status = ?,
phone_number = ?,
email = ?,
@@ -103,7 +101,6 @@ const update = async (worker, callback) => {
[
worker_name,
job_type,
formatDate(join_date),
status,
phone_number,
email,
@@ -116,7 +113,7 @@ const update = async (worker, callback) => {
callback(null, result.affectedRows);
} catch (err) {
callback(new Error(err.message || String(err)));
callback(new Error(err.message || String(err)));
}
};

View File

@@ -234,7 +234,7 @@ const generateCacheKey = (query, params = [], prefix = 'query') => {
*/
const optimizedQueries = {
// 작업자 목록 (페이지네이션)
getWorkersPaged: async (page = 1, limit = 10, search = '') => {
getWorkersPaged: async (page = 1, limit = 10, search = '', status = '') => {
let baseQuery = `
SELECT w.*, COUNT(dwr.id) as report_count
FROM workers w
@@ -243,16 +243,29 @@ const optimizedQueries = {
let countQuery = 'SELECT COUNT(*) as total FROM workers w';
let params = [];
let conditions = [];
// 검색 조건
if (search) {
const searchCondition = ' WHERE w.worker_name LIKE ? OR w.position LIKE ?';
baseQuery += searchCondition + ' GROUP BY w.worker_id';
countQuery += searchCondition;
params = [`%${search}%`, `%${search}%`];
} else {
baseQuery += ' GROUP BY w.worker_id';
conditions.push('(w.worker_name LIKE ? OR w.position LIKE ?)');
params.push(`%${search}%`, `%${search}%`);
}
// 상태 조건
if (status) {
conditions.push('w.status = ?');
params.push(status);
}
// 조건 조합
if (conditions.length > 0) {
const whereClause = ' WHERE ' + conditions.join(' AND ');
baseQuery += whereClause;
countQuery += whereClause;
}
baseQuery += ' GROUP BY w.worker_id';
return executePagedQuery(baseQuery, countQuery, params, {
page, limit, orderBy: 'w.worker_id', orderDirection: 'DESC'
});

View File

@@ -117,14 +117,14 @@ function showSaveResultModal(type, title, message, details = null) {
modal.style.display = 'flex';
// ESC 키로 닫기
document.addEventListener('keydown', function(e) {
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape') {
closeSaveResultModal();
}
});
// 배경 클릭으로 닫기
modal.addEventListener('click', function(e) {
modal.addEventListener('click', function (e) {
if (e.target === modal) {
closeSaveResultModal();
}
@@ -206,7 +206,8 @@ async function loadData() {
async function loadWorkers() {
try {
console.log('Workers API 호출 중... (통합 API 사용)');
const data = await window.apiCall(`${window.API}/workers`);
// 활성 작업자 1000명까지 조회 (서버 사이드 필터링 적용)
const data = await window.apiCall(`${window.API}/workers?status=active&limit=1000`);
const allWorkers = Array.isArray(data) ? data : (data.data || data.workers || []);
// 활성화된 작업자만 필터링
@@ -245,9 +246,9 @@ async function loadWorkTypes() {
} catch (error) {
console.log('⚠️ 작업 유형 API 사용 불가, 기본값 사용');
workTypes = [
{id: 1, name: 'Base'},
{id: 2, name: 'Vessel'},
{id: 3, name: 'Piping'}
{ id: 1, name: 'Base' },
{ id: 2, name: 'Vessel' },
{ id: 3, name: 'Piping' }
];
}
}
@@ -264,8 +265,8 @@ async function loadWorkStatusTypes() {
} catch (error) {
console.log('⚠️ 업무 상태 유형 API 사용 불가, 기본값 사용');
workStatusTypes = [
{id: 1, name: '정규'},
{id: 2, name: '에러'}
{ id: 1, name: '정규' },
{ id: 2, name: '에러' }
];
}
}
@@ -282,10 +283,10 @@ async function loadErrorTypes() {
} catch (error) {
console.log('⚠️ 에러 유형 API 사용 불가, 기본값 사용');
errorTypes = [
{id: 1, name: '설계미스'},
{id: 2, name: '외주작업 불량'},
{id: 3, name: '입고지연'},
{id: 4, name: '작업 불량'}
{ id: 1, name: '설계미스' },
{ id: 2, name: '외주작업 불량' },
{ id: 3, name: '입고지연' },
{ id: 4, name: '작업 불량' }
];
}
}
@@ -602,15 +603,15 @@ async function saveWorkReport() {
work_hours: parseFloat(workHours)
};
console.log('🔍 생성된 작업 항목:', workEntry);
console.log('🔍 작업 항목 상세:', {
project_id: workEntry.project_id,
work_type_id: workEntry.work_type_id,
work_status_id: workEntry.work_status_id,
error_type_id: workEntry.error_type_id,
work_hours: workEntry.work_hours
});
newWorkEntries.push(workEntry);
console.log('🔍 생성된 작업 항목:', workEntry);
console.log('🔍 작업 항목 상세:', {
project_id: workEntry.project_id,
work_type_id: workEntry.work_type_id,
work_status_id: workEntry.work_status_id,
error_type_id: workEntry.error_type_id,
work_hours: workEntry.work_hours
});
newWorkEntries.push(workEntry);
}
console.log('🔍 최종 수집된 작업 항목들:', newWorkEntries);