From 48fff7df6469e638c95ab61f2daf448204a4a3d5 Mon Sep 17 00:00:00 2001 From: Hyungi Ahn Date: Tue, 6 Jan 2026 15:50:40 +0900 Subject: [PATCH] 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 --- DEV_LOG.md | 14 ++ .../controllers/projectController.js | 10 + .../controllers/workerController.js | 10 +- api.hyungi.net/models/workerModel.js | 19 +- api.hyungi.net/utils/queryOptimizer.js | 131 +++++----- web-ui/js/daily-work-report.js | 229 +++++++++--------- 6 files changed, 226 insertions(+), 187 deletions(-) diff --git a/DEV_LOG.md b/DEV_LOG.md index 9f4ede8..0887e7c 100644 --- a/DEV_LOG.md +++ b/DEV_LOG.md @@ -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) diff --git a/api.hyungi.net/controllers/projectController.js b/api.hyungi.net/controllers/projectController.js index b1510a0..5cfea4d 100644 --- a/api.hyungi.net/controllers/projectController.js +++ b/api.hyungi.net/controllers/projectController.js @@ -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({ diff --git a/api.hyungi.net/controllers/workerController.js b/api.hyungi.net/controllers/workerController.js index 2c82f62..b356073 100644 --- a/api.hyungi.net/controllers/workerController.js +++ b/api.hyungi.net/controllers/workerController.js @@ -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({ diff --git a/api.hyungi.net/models/workerModel.js b/api.hyungi.net/models/workerModel.js index e39d3c3..96554dd 100644 --- a/api.hyungi.net/models/workerModel.js +++ b/api.hyungi.net/models/workerModel.js @@ -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))); } }; @@ -124,12 +121,12 @@ const update = async (worker, callback) => { const remove = async (worker_id, callback) => { const db = await getDb(); const conn = await db.getConnection(); - + try { await conn.beginTransaction(); - + console.log(`🗑️ 작업자 삭제 시작: worker_id=${worker_id}`); - + // 안전한 삭제: 각 테이블을 개별적으로 처리하고 오류가 발생해도 계속 진행 const tables = [ { name: 'users', query: 'UPDATE users SET worker_id = NULL WHERE worker_id = ?', action: '업데이트' }, @@ -142,7 +139,7 @@ const remove = async (worker_id, callback) => { { name: 'monthly_worker_status', query: 'DELETE FROM monthly_worker_status WHERE worker_id = ?', action: '삭제' }, { name: 'worker_groups', query: 'DELETE FROM worker_groups WHERE worker_id = ?', action: '삭제' } ]; - + for (const table of tables) { try { const [result] = await conn.query(table.query, [worker_id]); @@ -153,17 +150,17 @@ const remove = async (worker_id, callback) => { console.log(`⚠️ ${table.name} 테이블 ${table.action} 실패 (무시): ${tableError.message}`); } } - + // 마지막으로 작업자 삭제 const [result] = await conn.query( `DELETE FROM workers WHERE worker_id = ?`, [worker_id] ); console.log(`✅ 작업자 삭제 완료: ${result.affectedRows}건`); - + await conn.commit(); callback(null, result.affectedRows); - + } catch (err) { await conn.rollback(); console.error(`❌ 작업자 삭제 오류 (worker_id: ${worker_id}):`, err); diff --git a/api.hyungi.net/utils/queryOptimizer.js b/api.hyungi.net/utils/queryOptimizer.js index c6711da..9ded9f2 100644 --- a/api.hyungi.net/utils/queryOptimizer.js +++ b/api.hyungi.net/utils/queryOptimizer.js @@ -9,7 +9,7 @@ const paginate = (page = 1, limit = 10) => { const pageNum = Math.max(1, parseInt(page)); const limitNum = Math.min(100, Math.max(1, parseInt(limit))); // 최대 100개 제한 const offset = (pageNum - 1) * limitNum; - + return { limit: limitNum, offset, @@ -23,21 +23,21 @@ const paginate = (page = 1, limit = 10) => { const executePagedQuery = async (baseQuery, countQuery, params = [], options = {}) => { const { page = 1, limit = 10, orderBy = 'id', orderDirection = 'DESC' } = options; const { limit: limitNum, offset, page: pageNum } = paginate(page, limit); - + try { const db = await getDb(); - + // 전체 개수 조회 const [countResult] = await db.execute(countQuery, params); const totalCount = countResult[0]?.total || 0; - + // 데이터 조회 (ORDER BY와 LIMIT 추가) const pagedQuery = `${baseQuery} ORDER BY ${orderBy} ${orderDirection} LIMIT ${limitNum} OFFSET ${offset}`; const [rows] = await db.execute(pagedQuery, params); - + // 페이지네이션 메타데이터 계산 const totalPages = Math.ceil(totalCount / limitNum); - + return { data: rows, pagination: { @@ -49,7 +49,7 @@ const executePagedQuery = async (baseQuery, countQuery, params = [], options = { hasPrevPage: pageNum > 1 } }; - + } catch (error) { throw new Error(`페이지네이션 쿼리 실행 오류: ${error.message}`); } @@ -61,20 +61,20 @@ const executePagedQuery = async (baseQuery, countQuery, params = [], options = { const suggestIndexes = async (tableName) => { try { const db = await getDb(); - + // 현재 인덱스 조회 const [indexes] = await db.execute(`SHOW INDEX FROM ${tableName}`); - + // 테이블 구조 조회 const [columns] = await db.execute(`DESCRIBE ${tableName}`); - + const suggestions = []; - + // 외래키 컬럼에 인덱스 제안 - const foreignKeyColumns = columns.filter(col => + const foreignKeyColumns = columns.filter(col => col.Field.endsWith('_id') && !indexes.some(idx => idx.Column_name === col.Field) ); - + foreignKeyColumns.forEach(col => { suggestions.push({ type: 'INDEX', @@ -83,13 +83,13 @@ const suggestIndexes = async (tableName) => { sql: `CREATE INDEX idx_${tableName}_${col.Field} ON ${tableName}(${col.Field});` }); }); - + // 날짜 컬럼에 인덱스 제안 - const dateColumns = columns.filter(col => - (col.Type.includes('date') || col.Type.includes('timestamp')) && + const dateColumns = columns.filter(col => + (col.Type.includes('date') || col.Type.includes('timestamp')) && !indexes.some(idx => idx.Column_name === col.Field) ); - + dateColumns.forEach(col => { suggestions.push({ type: 'INDEX', @@ -98,7 +98,7 @@ const suggestIndexes = async (tableName) => { sql: `CREATE INDEX idx_${tableName}_${col.Field} ON ${tableName}(${col.Field});` }); }); - + return { tableName, currentIndexes: indexes.map(idx => ({ @@ -108,7 +108,7 @@ const suggestIndexes = async (tableName) => { })), suggestions }; - + } catch (error) { throw new Error(`인덱스 분석 오류: ${error.message}`); } @@ -120,23 +120,23 @@ const suggestIndexes = async (tableName) => { const analyzeQuery = async (query, params = []) => { try { const db = await getDb(); - + // EXPLAIN 실행 const explainQuery = `EXPLAIN ${query}`; const [explainResult] = await db.execute(explainQuery, params); - + // 쿼리 실행 시간 측정 const startTime = Date.now(); await db.execute(query, params); const executionTime = Date.now() - startTime; - + // 성능 분석 const analysis = { executionTime, explainResult, recommendations: [] }; - + // 성능 권장사항 생성 explainResult.forEach(row => { if (row.type === 'ALL') { @@ -146,7 +146,7 @@ const analyzeQuery = async (query, params = []) => { suggestion: '적절한 인덱스 추가 권장' }); } - + if (row.rows > 1000) { analysis.recommendations.push({ type: 'WARNING', @@ -154,7 +154,7 @@ const analyzeQuery = async (query, params = []) => { suggestion: 'WHERE 조건 최적화 또는 인덱스 추가 권장' }); } - + if (row.Extra && row.Extra.includes('Using filesort')) { analysis.recommendations.push({ type: 'INFO', @@ -163,9 +163,9 @@ const analyzeQuery = async (query, params = []) => { }); } }); - + return analysis; - + } catch (error) { throw new Error(`쿼리 분석 오류: ${error.message}`); } @@ -178,39 +178,39 @@ const batchInsert = async (tableName, data, batchSize = 100) => { if (!Array.isArray(data) || data.length === 0) { throw new Error('삽입할 데이터가 없습니다.'); } - + try { const db = await getDb(); const connection = await db.getConnection(); - + await connection.beginTransaction(); - + const columns = Object.keys(data[0]); const placeholders = columns.map(() => '?').join(', '); const insertQuery = `INSERT INTO ${tableName} (${columns.join(', ')}) VALUES (${placeholders})`; - + let insertedCount = 0; - + // 배치 단위로 처리 for (let i = 0; i < data.length; i += batchSize) { const batch = data.slice(i, i + batchSize); - + for (const row of batch) { const values = columns.map(col => row[col]); await connection.execute(insertQuery, values); insertedCount++; } } - + await connection.commit(); connection.release(); - + return { insertedCount, batchSize, totalBatches: Math.ceil(data.length / batchSize) }; - + } catch (error) { throw new Error(`배치 삽입 오류: ${error.message}`); } @@ -225,7 +225,7 @@ const generateCacheKey = (query, params = [], prefix = 'query') => { .createHash('md5') .update(query + paramString) .digest('hex'); - + return `${prefix}:${queryHash}`; }; @@ -234,30 +234,43 @@ 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 LEFT JOIN daily_work_reports dwr ON w.worker_id = dwr.worker_id `; - + 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' }); }, - + // 프로젝트 목록 (페이지네이션) getProjectsPaged: async (page = 1, limit = 10, status = '') => { let baseQuery = ` @@ -266,10 +279,10 @@ const optimizedQueries = { FROM projects p LEFT JOIN daily_work_reports dwr ON p.project_id = dwr.project_id `; - + let countQuery = 'SELECT COUNT(*) as total FROM projects p'; let params = []; - + if (status) { const statusCondition = ' WHERE p.status = ?'; baseQuery += statusCondition + ' GROUP BY p.project_id'; @@ -278,12 +291,12 @@ const optimizedQueries = { } else { baseQuery += ' GROUP BY p.project_id'; } - + return executePagedQuery(baseQuery, countQuery, params, { page, limit, orderBy: 'p.project_id', orderDirection: 'DESC' }); }, - + // 일일 작업 보고서 (날짜 범위, 페이지네이션) getDailyWorkReportsPaged: async (startDate, endDate, page = 1, limit = 10) => { const baseQuery = ` @@ -299,13 +312,13 @@ const optimizedQueries = { LEFT JOIN users u ON dwr.created_by = u.user_id WHERE dwr.report_date BETWEEN ? AND ? `; - + const countQuery = ` SELECT COUNT(*) as total FROM daily_work_reports dwr WHERE dwr.report_date BETWEEN ? AND ? `; - + return executePagedQuery(baseQuery, countQuery, [startDate, endDate], { page, limit, orderBy: 'dwr.report_date', orderDirection: 'DESC' }); @@ -318,11 +331,11 @@ const optimizedQueries = { const getPerformanceStats = async () => { try { const db = await getDb(); - + // 연결 상태 조회 const [connections] = await db.execute('SHOW STATUS LIKE "Threads_connected"'); const [maxConnections] = await db.execute('SHOW VARIABLES LIKE "max_connections"'); - + // 쿼리 캐시 상태 (MySQL 8.0 이전 버전) let queryCacheStats = null; try { @@ -331,10 +344,10 @@ const getPerformanceStats = async () => { } catch (error) { // MySQL 8.0+에서는 쿼리 캐시가 제거됨 } - + // 슬로우 쿼리 로그 상태 const [slowQueries] = await db.execute('SHOW STATUS LIKE "Slow_queries"'); - + return { connections: { current: parseInt(connections[0]?.Value || 0), @@ -344,7 +357,7 @@ const getPerformanceStats = async () => { slowQueries: parseInt(slowQueries[0]?.Value || 0), timestamp: new Date().toISOString() }; - + } catch (error) { throw new Error(`성능 통계 조회 오류: ${error.message}`); } diff --git a/web-ui/js/daily-work-report.js b/web-ui/js/daily-work-report.js index 894b59b..aeb8d53 100644 --- a/web-ui/js/daily-work-report.js +++ b/web-ui/js/daily-work-report.js @@ -30,7 +30,7 @@ function getCurrentUser() { try { const token = localStorage.getItem('token'); if (!token) return null; - + const payloadBase64 = token.split('.')[1]; if (payloadBase64) { const payload = JSON.parse(atob(payloadBase64)); @@ -40,7 +40,7 @@ function getCurrentUser() { } catch (error) { console.log('토큰에서 사용자 정보 추출 실패:', error); } - + try { const userInfo = localStorage.getItem('user') || localStorage.getItem('userInfo') || localStorage.getItem('currentUser'); if (userInfo) { @@ -51,7 +51,7 @@ function getCurrentUser() { } catch (error) { console.log('localStorage에서 사용자 정보 가져오기 실패:', error); } - + return null; } @@ -59,7 +59,7 @@ function getCurrentUser() { function showMessage(message, type = 'info') { const container = document.getElementById('message-container'); container.innerHTML = `
${message}
`; - + if (type === 'success') { setTimeout(() => { hideMessage(); @@ -76,7 +76,7 @@ function showSaveResultModal(type, title, message, details = null) { const modal = document.getElementById('saveResultModal'); const titleElement = document.getElementById('resultModalTitle'); const contentElement = document.getElementById('resultModalContent'); - + // 아이콘 설정 let icon = ''; switch (type) { @@ -92,14 +92,14 @@ function showSaveResultModal(type, title, message, details = null) { default: icon = 'ℹ️'; } - + // 모달 내용 구성 let content = `
${icon}

${title}

${message}

`; - + // 상세 정보가 있으면 추가 if (details && details.length > 0) { content += ` @@ -111,20 +111,20 @@ function showSaveResultModal(type, title, message, details = null) { `; } - + titleElement.textContent = '저장 결과'; contentElement.innerHTML = content; 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(); } @@ -135,7 +135,7 @@ function showSaveResultModal(type, title, message, details = null) { function closeSaveResultModal() { const modal = document.getElementById('saveResultModal'); modal.style.display = 'none'; - + // 이벤트 리스너 제거 document.removeEventListener('keydown', closeSaveResultModal); } @@ -155,10 +155,10 @@ function goToStep(stepNumber) { } } } - + // 진행 단계 표시 업데이트 updateProgressSteps(stepNumber); - + currentStep = stepNumber; } @@ -168,7 +168,7 @@ function updateProgressSteps(currentStepNumber) { const progressStep = document.getElementById(`progressStep${i}`); if (progressStep) { progressStep.classList.remove('active', 'completed'); - + if (i < currentStepNumber) { progressStep.classList.add('completed'); } else if (i === currentStepNumber) { @@ -189,14 +189,14 @@ async function loadData() { await loadWorkTypes(); await loadWorkStatusTypes(); await loadErrorTypes(); - + console.log('로드된 작업자 수:', workers.length); console.log('로드된 프로젝트 수:', projects.length); console.log('작업 유형 수:', workTypes.length); populateWorkerGrid(); hideMessage(); - + } catch (error) { console.error('데이터 로드 실패:', error); showMessage('데이터 로드 중 오류가 발생했습니다: ' + error.message, 'error'); @@ -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: '작업 불량' } ]; } } @@ -294,18 +295,18 @@ async function loadErrorTypes() { function populateWorkerGrid() { const grid = document.getElementById('workerGrid'); grid.innerHTML = ''; - + workers.forEach(worker => { const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'worker-card'; btn.textContent = worker.worker_name; btn.dataset.id = worker.worker_id; - + btn.addEventListener('click', () => { toggleWorkerSelection(worker.worker_id, btn); }); - + grid.appendChild(btn); }); } @@ -319,7 +320,7 @@ function toggleWorkerSelection(workerId, btnElement) { selectedWorkers.add(workerId); btnElement.classList.add('selected'); } - + const nextBtn = document.getElementById('nextStep2'); nextBtn.disabled = selectedWorkers.size === 0; } @@ -331,12 +332,12 @@ function addWorkEntry() { console.log('🔧 컨테이너:', container); workEntryCounter++; console.log('🔧 작업 항목 카운터:', workEntryCounter); - + const entryDiv = document.createElement('div'); entryDiv.className = 'work-entry'; entryDiv.dataset.id = workEntryCounter; console.log('🔧 생성된 작업 항목 div:', entryDiv); - + entryDiv.innerHTML = `
작업 항목 #${workEntryCounter}
@@ -414,12 +415,12 @@ function addWorkEntry() {
`; - + container.appendChild(entryDiv); console.log('🔧 작업 항목이 컨테이너에 추가됨'); console.log('🔧 현재 컨테이너 내용:', container.innerHTML.length, '문자'); console.log('🔧 현재 .work-entry 개수:', container.querySelectorAll('.work-entry').length); - + setupWorkEntryEvents(entryDiv); console.log('🔧 이벤트 설정 완료'); } @@ -430,7 +431,7 @@ function setupWorkEntryEvents(entryDiv) { const workStatusSelect = entryDiv.querySelector('.work-status-select'); const errorTypeSection = entryDiv.querySelector('.error-type-section'); const errorTypeSelect = entryDiv.querySelector('.error-type-select'); - + // 시간 입력 이벤트 timeInput.addEventListener('input', updateTotalHours); @@ -440,7 +441,7 @@ function setupWorkEntryEvents(entryDiv) { e.preventDefault(); timeInput.value = btn.dataset.hours; updateTotalHours(); - + // 버튼 클릭 효과 btn.style.transform = 'scale(0.95)'; setTimeout(() => { @@ -452,11 +453,11 @@ function setupWorkEntryEvents(entryDiv) { // 업무 상태 변경 시 에러 유형 섹션 토글 workStatusSelect.addEventListener('change', (e) => { const isError = e.target.value === '2'; // 에러 상태 ID가 2라고 가정 - + if (isError) { errorTypeSection.classList.add('visible'); errorTypeSelect.required = true; - + // 에러 상태일 때 시각적 피드백 errorTypeSection.style.animation = 'slideDown 0.4s ease-out'; } else { @@ -465,7 +466,7 @@ function setupWorkEntryEvents(entryDiv) { errorTypeSelect.value = ''; } }); - + // 폼 필드 포커스 효과 entryDiv.querySelectorAll('.form-field-group').forEach(group => { const input = group.querySelector('select, input'); @@ -473,7 +474,7 @@ function setupWorkEntryEvents(entryDiv) { input.addEventListener('focus', () => { group.classList.add('focused'); }); - + input.addEventListener('blur', () => { group.classList.remove('focused'); }); @@ -499,15 +500,15 @@ function removeWorkEntry(id) { function updateTotalHours() { const timeInputs = document.querySelectorAll('.time-input'); let total = 0; - + timeInputs.forEach(input => { const value = parseFloat(input.value) || 0; total += value; }); - + const display = document.getElementById('totalHoursDisplay'); display.textContent = `총 작업시간: ${total}시간`; - + if (total > 24) { display.style.background = 'linear-gradient(135deg, #e74c3c 0%, #c0392b 100%)'; display.textContent += ' ⚠️ 24시간 초과'; @@ -532,7 +533,7 @@ async function saveWorkReport() { const entries = document.querySelectorAll('.work-entry'); console.log('🔍 찾은 작업 항목들:', entries); console.log('🔍 작업 항목 개수:', entries.length); - + if (entries.length === 0) { showSaveResultModal( 'error', @@ -544,16 +545,16 @@ async function saveWorkReport() { const newWorkEntries = []; console.log('🔍 작업 항목 수집 시작...'); - + for (const entry of entries) { console.log('🔍 작업 항목 처리 중:', entry); - + const projectSelect = entry.querySelector('.project-select'); const workTypeSelect = entry.querySelector('.work-type-select'); const workStatusSelect = entry.querySelector('.work-status-select'); const errorTypeSelect = entry.querySelector('.error-type-select'); const timeInput = entry.querySelector('.time-input'); - + console.log('🔍 선택된 요소들:', { projectSelect, workTypeSelect, @@ -561,13 +562,13 @@ async function saveWorkReport() { errorTypeSelect, timeInput }); - + const projectId = projectSelect?.value; const workTypeId = workTypeSelect?.value; const workStatusId = workStatusSelect?.value; const errorTypeId = errorTypeSelect?.value; const workHours = timeInput?.value; - + console.log('🔍 수집된 값들:', { projectId, workTypeId, @@ -575,7 +576,7 @@ async function saveWorkReport() { errorTypeId, workHours }); - + if (!projectId || !workTypeId || !workStatusId || !workHours) { showSaveResultModal( 'error', @@ -593,7 +594,7 @@ async function saveWorkReport() { ); return; } - + const workEntry = { project_id: parseInt(projectId), work_type_id: parseInt(workTypeId), @@ -601,18 +602,18 @@ async function saveWorkReport() { error_type_id: errorTypeId ? parseInt(errorTypeId) : null, 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); console.log('🔍 총 작업 항목 개수:', newWorkEntries.length); @@ -628,7 +629,7 @@ async function saveWorkReport() { for (const workerId of selectedWorkers) { const workerName = workers.find(w => w.worker_id == workerId)?.worker_name || '알 수 없음'; - + // 서버가 기대하는 work_entries 배열 형태로 전송 const requestData = { report_date: reportDate, @@ -656,11 +657,11 @@ async function saveWorkReport() { } catch (error) { console.error('❌ 저장 실패:', error); totalFailed++; - + failureDetails.push(`${workerName}: ${error.message}`); } } - + // 결과 모달 표시 if (totalSaved > 0 && totalFailed === 0) { showSaveResultModal( @@ -683,14 +684,14 @@ async function saveWorkReport() { failureDetails ); } - + if (totalSaved > 0) { setTimeout(() => { refreshTodayWorkers(); resetForm(); }, 2000); } - + } catch (error) { console.error('저장 오류:', error); showSaveResultModal( @@ -709,18 +710,18 @@ async function saveWorkReport() { // 폼 초기화 function resetForm() { goToStep(1); - + selectedWorkers.clear(); document.querySelectorAll('.worker-card.selected').forEach(btn => { btn.classList.remove('selected'); }); - + const container = document.getElementById('workEntriesList'); container.innerHTML = ''; - + workEntryCounter = 0; updateTotalHours(); - + document.getElementById('nextStep2').disabled = true; } @@ -728,19 +729,19 @@ function resetForm() { async function loadTodayWorkers() { const section = document.getElementById('dailyWorkersSection'); const content = document.getElementById('dailyWorkersContent'); - + if (!section || !content) { console.log('당일 현황 섹션이 HTML에 없습니다.'); return; } - + try { const today = getKoreaToday(); const currentUser = getCurrentUser(); - + content.innerHTML = '
📊 내가 입력한 오늘의 작업 현황을 불러오는 중... (통합 API)
'; section.style.display = 'block'; - + // 본인이 입력한 데이터만 조회 (통합 API 사용) let queryParams = `date=${today}`; if (currentUser?.user_id) { @@ -748,21 +749,21 @@ async function loadTodayWorkers() { } else if (currentUser?.id) { queryParams += `&created_by=${currentUser.id}`; } - + console.log(`🔒 본인 입력분만 조회 (통합 API): ${API}/daily-work-reports?${queryParams}`); - + const rawData = await window.apiCall(`${window.API}/daily-work-reports?${queryParams}`); console.log('📊 당일 작업 데이터 (통합 API):', rawData); - + let data = []; if (Array.isArray(rawData)) { data = rawData; } else if (rawData?.data) { data = rawData.data; } - + displayMyDailyWorkers(data, today); - + } catch (error) { console.error('당일 작업자 로드 오류:', error); content.innerHTML = ` @@ -777,7 +778,7 @@ async function loadTodayWorkers() { // 본인 입력 작업자 현황 표시 (수정/삭제 기능 포함) function displayMyDailyWorkers(data, date) { const content = document.getElementById('dailyWorkersContent'); - + if (!Array.isArray(data) || data.length === 0) { content.innerHTML = `
@@ -787,7 +788,7 @@ function displayMyDailyWorkers(data, date) { `; return; } - + // 작업자별로 데이터 그룹화 const workerGroups = {}; data.forEach(work => { @@ -797,10 +798,10 @@ function displayMyDailyWorkers(data, date) { } workerGroups[workerName].push(work); }); - + const totalWorkers = Object.keys(workerGroups).length; const totalWorks = data.length; - + const headerHtml = `

📊 내가 입력한 오늘(${date}) 작업 현황 - 총 ${totalWorkers}명, ${totalWorks}개 작업

@@ -809,12 +810,12 @@ function displayMyDailyWorkers(data, date) {
`; - + const workersHtml = Object.entries(workerGroups).map(([workerName, works]) => { const totalHours = works.reduce((sum, work) => { return sum + parseFloat(work.work_hours || 0); }, 0); - + // 개별 작업 항목들 (수정/삭제 버튼 포함) const individualWorksHtml = works.map((work) => { const projectName = work.project_name || '미지정'; @@ -823,7 +824,7 @@ function displayMyDailyWorkers(data, date) { const workHours = work.work_hours || 0; const errorTypeName = work.error_type_name || null; const workId = work.id; - + return `
@@ -861,7 +862,7 @@ function displayMyDailyWorkers(data, date) {
`; }).join(''); - + return `
@@ -874,7 +875,7 @@ function displayMyDailyWorkers(data, date) {
`; }).join(''); - + content.innerHTML = headerHtml + '
' + workersHtml + '
'; } @@ -882,17 +883,17 @@ function displayMyDailyWorkers(data, date) { async function editWorkItem(workId) { try { console.log('수정할 작업 ID:', workId); - + // 1. 기존 데이터 조회 (통합 API 사용) showMessage('작업 정보를 불러오는 중... (통합 API)', 'loading'); - + const workData = await window.apiCall(`${window.API}/daily-work-reports/${workId}`); console.log('수정할 작업 데이터 (통합 API):', workData); - + // 2. 수정 모달 표시 showEditModal(workData); hideMessage(); - + } catch (error) { console.error('작업 정보 조회 오류:', error); showMessage('작업 정보를 불러올 수 없습니다: ' + error.message, 'error'); @@ -902,7 +903,7 @@ async function editWorkItem(workId) { // 수정 모달 표시 function showEditModal(workData) { editingWorkId = workData.id; - + const modalHtml = `
@@ -973,9 +974,9 @@ function showEditModal(workData) {
`; - + document.body.insertAdjacentHTML('beforeend', modalHtml); - + // 업무 상태 변경 이벤트 document.getElementById('editWorkStatus').addEventListener('change', (e) => { const errorTypeGroup = document.getElementById('editErrorTypeGroup'); @@ -1004,17 +1005,17 @@ async function saveEditedWork() { const workStatusId = document.getElementById('editWorkStatus').value; const errorTypeId = document.getElementById('editErrorType').value; const workHours = document.getElementById('editWorkHours').value; - + if (!projectId || !workTypeId || !workStatusId || !workHours) { showMessage('모든 필수 항목을 입력해주세요.', 'error'); return; } - + if (workStatusId === '2' && !errorTypeId) { showMessage('에러 상태인 경우 에러 유형을 선택해주세요.', 'error'); return; } - + const updateData = { project_id: parseInt(projectId), work_type_id: parseInt(workTypeId), @@ -1022,20 +1023,20 @@ async function saveEditedWork() { error_type_id: errorTypeId ? parseInt(errorTypeId) : null, work_hours: parseFloat(workHours) }; - + showMessage('작업을 수정하는 중... (통합 API)', 'loading'); - + const result = await window.apiCall(`${window.API}/daily-work-reports/${editingWorkId}`, { method: 'PUT', body: JSON.stringify(updateData) }); - + console.log('✅ 수정 성공 (통합 API):', result); showMessage('✅ 작업이 성공적으로 수정되었습니다!', 'success'); - + closeEditModal(); refreshTodayWorkers(); - + } catch (error) { console.error('❌ 수정 실패:', error); showMessage('수정 중 오류가 발생했습니다: ' + error.message, 'error'); @@ -1047,23 +1048,23 @@ async function deleteWorkItem(workId) { if (!confirm('정말로 이 작업을 삭제하시겠습니까?\n삭제된 작업은 복구할 수 없습니다.')) { return; } - + try { console.log('삭제할 작업 ID:', workId); - + showMessage('작업을 삭제하는 중... (통합 API)', 'loading'); - + // 개별 항목 삭제 API 호출 (본인 작성분만 삭제 가능) - 통합 API 사용 const result = await window.apiCall(`${window.API}/daily-work-reports/my-entry/${workId}`, { method: 'DELETE' }); - + console.log('✅ 삭제 성공 (통합 API):', result); showMessage('✅ 작업이 성공적으로 삭제되었습니다!', 'success'); - + // 화면 새로고침 refreshTodayWorkers(); - + } catch (error) { console.error('❌ 삭제 실패:', error); showMessage('삭제 중 오류가 발생했습니다: ' + error.message, 'error'); @@ -1117,9 +1118,9 @@ async function init() { await loadData(); setupEventListeners(); loadTodayWorkers(); - + console.log('✅ 시스템 초기화 완료 (통합 API 설정 적용)'); - + } catch (error) { console.error('초기화 오류:', error); showMessage('초기화 중 오류가 발생했습니다.', 'error');