From bea62dfdee0ba3cef623a69f4a6eafa178a4ae28 Mon Sep 17 00:00:00 2001 From: Hyungi Ahn Date: Mon, 19 Jan 2026 10:17:31 +0900 Subject: [PATCH] =?UTF-8?q?refactor:=20=EC=9E=91=EC=97=85=EC=9E=90=20?= =?UTF-8?q?=EA=B4=80=EB=A6=AC=20=EA=B0=9C=EC=84=A0=20-=20=EA=B3=84?= =?UTF-8?q?=EC=A0=95=20=EC=97=B0=EB=8F=99=20=EA=B8=B0=EB=8A=A5=EC=9C=BC?= =?UTF-8?q?=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 작업 보고서 표시 여부 대신 계정 연동 기능으로 개선했습니다. ## 주요 변경사항 ### 개념 변경 - **이전**: 작업 보고서 표시 여부 (show_in_work_reports) - **이후**: 계정 생성/연동 기능 ### 데이터베이스 - **마이그레이션**: 20260119095549_add_worker_display_fields.js - show_in_work_reports 컬럼 제거 - employment_status만 유지 (employed/resigned) - **workerModel**: - getAll, getById에서 users 테이블 JOIN하여 user_id 조회 - create, update에서 show_in_work_reports 필드 제거 ### 백엔드 API - **workerController.js**: - createWorker: create_account 체크 시 자동으로 users 테이블에 계정 생성 - username: hangulToRoman으로 한글 이름 변환 - password: 초기 비밀번호 '1234' (bcrypt 해시) - role: User 역할 자동 할당 - updateWorker: - create_account=true & 계정 없음 → 계정 생성 - create_account=false & 계정 있음 → 계정 연동 해제 (users.worker_id=NULL) ### 프론트엔드 - **worker-management.html**: - "작업 보고서 표시" → "🔐 계정 생성/연동"으로 변경 - 체크 시 로그인 계정 자동 생성 안내 - **worker-management.js**: - 카드 렌더링: user_id 존재 여부로 계정 연동 상태 표시 (🔐 아이콘) - saveWorker: create_account 필드 전송 - show_in_work_reports 관련 로직 모두 제거 - **daily-work-report.js**: - 필터링 조건 단순화: 퇴사자만 제외 (employment_status≠resigned) - 계정 여부와 무관하게 모든 재직자 표시 ## 사용 방법 1. 작업자 등록/수정 시 "계정 생성/연동" 체크 2. 자동으로 로그인 계정 생성 (초기 비밀번호: 1234) 3. 계정이 있는 작업자는 나의 대시보드, 연차/출퇴근 관리 가능 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../controllers/workerController.js | 82 ++++++++++++++++++- ...0260119095549_add_worker_display_fields.js | 13 +-- api.hyungi.net/models/workerModel.js | 32 ++++---- web-ui/js/daily-work-report.js | 8 +- web-ui/js/worker-management.js | 26 +++--- .../pages/management/worker-management.html | 14 ++-- 6 files changed, 120 insertions(+), 55 deletions(-) diff --git a/api.hyungi.net/controllers/workerController.js b/api.hyungi.net/controllers/workerController.js index c8bdce5..9c8b20b 100644 --- a/api.hyungi.net/controllers/workerController.js +++ b/api.hyungi.net/controllers/workerController.js @@ -13,14 +13,18 @@ const { asyncHandler } = require('../middlewares/errorHandler'); const logger = require('../utils/logger'); const cache = require('../utils/cache'); const { optimizedQueries } = require('../utils/queryOptimizer'); +const { hangulToRoman, generateUniqueUsername } = require('../utils/hangulToRoman'); +const bcrypt = require('bcrypt'); +const { getDb } = require('../dbPool'); /** * 작업자 생성 */ exports.createWorker = asyncHandler(async (req, res) => { const workerData = req.body; + const createAccount = req.body.create_account; - logger.info('작업자 생성 요청', { name: workerData.name }); + logger.info('작업자 생성 요청', { name: workerData.worker_name, create_account: createAccount }); const lastID = await new Promise((resolve, reject) => { workerModel.create(workerData, (err, id) => { @@ -29,6 +33,30 @@ exports.createWorker = asyncHandler(async (req, res) => { }); }); + // 계정 생성 요청이 있으면 users 테이블에 계정 생성 + if (createAccount && workerData.worker_name) { + try { + const db = await getDb(); + const username = await generateUniqueUsername(workerData.worker_name, db); + const hashedPassword = await bcrypt.hash('1234', 10); + + // User 역할 조회 + const [userRole] = await db.query('SELECT id FROM roles WHERE name = ?', ['User']); + + if (userRole && userRole.length > 0) { + await db.query( + `INSERT INTO users (username, password, name, worker_id, role_id, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, NOW(), NOW())`, + [username, hashedPassword, workerData.worker_name, lastID, userRole[0].id] + ); + + logger.info('작업자 계정 자동 생성 성공', { worker_id: lastID, username }); + } + } catch (accountError) { + logger.error('계정 생성 실패 (작업자는 생성됨)', { worker_id: lastID, error: accountError.message }); + } + } + // 작업자 관련 캐시 무효화 await cache.invalidateCache.worker(); @@ -115,13 +143,28 @@ exports.updateWorker = asyncHandler(async (req, res) => { } const workerData = { ...req.body, worker_id: id }; + const createAccount = req.body.create_account; console.log('🔧 작업자 수정 요청:', { worker_id: id, 받은데이터: req.body, - 처리할데이터: workerData + 처리할데이터: workerData, + create_account: createAccount }); + // 먼저 현재 작업자 정보 조회 (계정 여부 확인용) + const currentWorker = await new Promise((resolve, reject) => { + workerModel.getById(id, (err, data) => { + if (err) reject(new DatabaseError('작업자 조회 중 오류가 발생했습니다')); + else resolve(data); + }); + }); + + if (!currentWorker) { + throw new NotFoundError('작업자를 찾을 수 없습니다'); + } + + // 작업자 정보 업데이트 const changes = await new Promise((resolve, reject) => { workerModel.update(workerData, (err, affected) => { if (err) { @@ -132,8 +175,39 @@ exports.updateWorker = asyncHandler(async (req, res) => { }); }); - if (changes === 0) { - throw new NotFoundError('작업자를 찾을 수 없습니다'); + // 계정 생성/해제 처리 + const db = await getDb(); + const hasAccount = currentWorker.user_id !== null && currentWorker.user_id !== undefined; + + if (createAccount && !hasAccount && workerData.worker_name) { + // 계정 생성 + try { + const username = await generateUniqueUsername(workerData.worker_name, db); + const hashedPassword = await bcrypt.hash('1234', 10); + + // User 역할 조회 + const [userRole] = await db.query('SELECT id FROM roles WHERE name = ?', ['User']); + + if (userRole && userRole.length > 0) { + await db.query( + `INSERT INTO users (username, password, name, worker_id, role_id, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, NOW(), NOW())`, + [username, hashedPassword, workerData.worker_name, id, userRole[0].id] + ); + + logger.info('작업자 계정 생성 성공', { worker_id: id, username }); + } + } catch (accountError) { + logger.error('계정 생성 실패', { worker_id: id, error: accountError.message }); + } + } else if (!createAccount && hasAccount) { + // 계정 연동 해제 (users.worker_id = NULL) + try { + await db.query('UPDATE users SET worker_id = NULL WHERE worker_id = ?', [id]); + logger.info('작업자 계정 연동 해제 성공', { worker_id: id }); + } catch (unlinkError) { + logger.error('계정 연동 해제 실패', { worker_id: id, error: unlinkError.message }); + } } // 작업자 관련 캐시 무효화 diff --git a/api.hyungi.net/db/migrations/20260119095549_add_worker_display_fields.js b/api.hyungi.net/db/migrations/20260119095549_add_worker_display_fields.js index 3a52b9e..4cb664d 100644 --- a/api.hyungi.net/db/migrations/20260119095549_add_worker_display_fields.js +++ b/api.hyungi.net/db/migrations/20260119095549_add_worker_display_fields.js @@ -4,20 +4,14 @@ */ 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('재직 상태'); + .comment('재직 상태 (employed: 재직, resigned: 퇴사)'); }); - console.log('✅ workers 테이블에 show_in_work_reports, employment_status 컬럼 추가 완료'); + console.log('✅ workers 테이블에 employment_status 컬럼 추가 완료'); }; /** @@ -26,9 +20,8 @@ exports.up = async function(knex) { */ 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 컬럼 삭제 완료'); + console.log('✅ workers 테이블에서 employment_status 컬럼 삭제 완료'); }; diff --git a/api.hyungi.net/models/workerModel.js b/api.hyungi.net/models/workerModel.js index f2ef1ab..c48c27d 100644 --- a/api.hyungi.net/models/workerModel.js +++ b/api.hyungi.net/models/workerModel.js @@ -20,15 +20,14 @@ const create = async (worker, callback) => { salary = null, annual_leave = null, status = 'active', - show_in_work_reports = true, employment_status = 'employed' } = worker; const [result] = await db.query( `INSERT INTO workers - (worker_name, job_type, join_date, salary, annual_leave, status, show_in_work_reports, employment_status) - VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, - [worker_name, job_type, formatDate(join_date), salary, annual_leave, status, show_in_work_reports, employment_status] + (worker_name, job_type, join_date, salary, annual_leave, status, employment_status) + VALUES (?, ?, ?, ?, ?, ?, ?)`, + [worker_name, job_type, formatDate(join_date), salary, annual_leave, status, employment_status] ); callback(null, result.insertId); @@ -44,10 +43,12 @@ const getAll = async (callback) => { const db = await getDb(); const [rows] = await db.query(` SELECT - *, - CASE WHEN status = 'active' THEN 1 ELSE 0 END AS is_active - FROM workers - ORDER BY worker_id DESC + w.*, + CASE WHEN w.status = 'active' THEN 1 ELSE 0 END AS is_active, + u.user_id + FROM workers w + LEFT JOIN users u ON w.worker_id = u.worker_id + ORDER BY w.worker_id DESC `); callback(null, rows); } catch (err) { @@ -61,10 +62,12 @@ const getById = async (worker_id, callback) => { const db = await getDb(); const [rows] = await db.query(` SELECT - *, - CASE WHEN status = 'active' THEN 1 ELSE 0 END AS is_active - FROM workers - WHERE worker_id = ? + w.*, + CASE WHEN w.status = 'active' THEN 1 ELSE 0 END AS is_active, + u.user_id + FROM workers w + LEFT JOIN users u ON w.worker_id = u.worker_id + WHERE w.worker_id = ? `, [worker_id]); callback(null, rows[0]); } catch (err) { @@ -84,7 +87,6 @@ const update = async (worker, callback) => { join_date, salary, annual_leave, - show_in_work_reports, employment_status } = worker; @@ -116,10 +118,6 @@ const update = async (worker, callback) => { updates.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); diff --git a/web-ui/js/daily-work-report.js b/web-ui/js/daily-work-report.js index a91223f..5037e64 100644 --- a/web-ui/js/daily-work-report.js +++ b/web-ui/js/daily-work-report.js @@ -211,16 +211,14 @@ async function loadWorkers() { const allWorkers = Array.isArray(data) ? data : (data.data || data.workers || []); // 작업 보고서에 표시할 작업자만 필터링 - // 1. show_in_work_reports가 true - // 2. employment_status가 resigned가 아님 + // 퇴사자만 제외 (계정 여부와 무관하게 재직자는 모두 표시) workers = allWorkers.filter(worker => { - const showInReports = worker.show_in_work_reports !== 0 && worker.show_in_work_reports !== false; const notResigned = worker.employment_status !== 'resigned'; - return showInReports && notResigned; + return notResigned; }); console.log(`✅ Workers 로드 성공: ${workers.length}명 (전체: ${allWorkers.length}명)`); - console.log(`📊 필터링 조건: show_in_work_reports=true, employment_status≠resigned`); + console.log(`📊 필터링 조건: employment_status≠resigned (퇴사자만 제외)`); } catch (error) { console.error('작업자 로딩 오류:', error); throw error; diff --git a/web-ui/js/worker-management.js b/web-ui/js/worker-management.js index 781c0de..1e5d78b 100644 --- a/web-ui/js/worker-management.js +++ b/web-ui/js/worker-management.js @@ -198,7 +198,7 @@ function renderWorkers() { const jobType = jobTypeMap[worker.job_type] || jobTypeMap['worker']; 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; + const hasAccount = worker.user_id !== null && worker.user_id !== undefined; console.log('🎨 카드 렌더링:', { worker_id: worker.worker_id, @@ -207,7 +207,8 @@ function renderWorkers() { is_active: worker.is_active, isInactive: isInactive, isResigned: isResigned, - showInWorkReports: showInWorkReports + user_id: worker.user_id, + hasAccount: hasAccount }); return ` @@ -221,17 +222,18 @@ function renderWorkers() {

${worker.worker_name} + ${hasAccount ? '🔐' : ''} ${isResigned ? '(퇴사)' : isInactive ? '(사무직)' : ''}

${jobType.icon} ${jobType.text} + ${hasAccount ? '🔐 계정 연동됨' : '⚪ 계정 없음'} ${worker.phone_number ? `📞 ${worker.phone_number}` : ''} ${worker.email ? `📧 ${worker.email}` : ''} ${worker.department ? `🏢 ${worker.department}` : ''} ${worker.hire_date ? `📅 입사: ${formatDate(worker.hire_date)}` : ''} - ${isResigned ? '⚠️ 퇴사 처리됨' : - !showInWorkReports ? '⚠️ 작업보고서에서 숨김' : ''} + ${isResigned ? '⚠️ 퇴사 처리됨' : ''}
@@ -456,9 +458,9 @@ function openWorkerModal(worker = null) { const isActiveValue = worker.status !== 'inactive' && worker.is_active !== 0 && worker.is_active !== false; 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; + // 계정 연동 여부 확인 (user_id가 있으면 계정 있음) + const hasAccountValue = worker.user_id !== null && worker.user_id !== undefined; + document.getElementById('hasAccount').checked = hasAccountValue; // employment_status 값 처리 (resigned이면 체크) const isResignedValue = worker.employment_status === 'resigned'; @@ -471,7 +473,8 @@ function openWorkerModal(worker = null) { status: worker.status, is_active_raw: worker.is_active, is_active_processed: isActiveValue, - show_in_work_reports: showInWorkReportsValue, + user_id: worker.user_id, + has_account: hasAccountValue, employment_status: worker.employment_status, is_resigned: isResignedValue }); @@ -484,7 +487,7 @@ function openWorkerModal(worker = null) { document.getElementById('workerForm').reset(); document.getElementById('workerId').value = ''; document.getElementById('isActive').checked = true; - document.getElementById('showInWorkReports').checked = true; + document.getElementById('hasAccount').checked = false; document.getElementById('isResigned').checked = false; } @@ -531,8 +534,8 @@ async function saveWorker() { salary: document.getElementById('salary')?.value || null, annual_leave: document.getElementById('annualLeave')?.value || null, status: document.getElementById('isActive').checked ? 'active' : 'inactive', - show_in_work_reports: document.getElementById('showInWorkReports').checked, - employment_status: document.getElementById('isResigned').checked ? 'resigned' : 'employed' + employment_status: document.getElementById('isResigned').checked ? 'resigned' : 'employed', + create_account: document.getElementById('hasAccount').checked // 계정 생성 여부 }; console.log('💾 저장할 작업자 데이터:', JSON.stringify(workerData, null, 2)); @@ -597,7 +600,6 @@ async function toggleWorkerStatus(workerId) { join_date: worker.join_date || null, salary: worker.salary || 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' }; diff --git a/web-ui/pages/management/worker-management.html b/web-ui/pages/management/worker-management.html index de8f470..af7b72a 100644 --- a/web-ui/pages/management/worker-management.html +++ b/web-ui/pages/management/worker-management.html @@ -176,19 +176,19 @@
- + - 체크 해제 시 일일 작업보고서 작성 시 이 작업자가 목록에 나타나지 않습니다 + 체크 시 로그인 계정이 자동 생성됩니다 (나의 대시보드, 연차/출퇴근 관리 가능) 체크: 현장직 (출퇴근 관리 필요) / 체크 해제: 사무직 (출퇴근 관리 불필요) @@ -197,10 +197,10 @@ - 퇴사한 작업자로 표시됩니다. 작업자 목록에서 별도로 표시됩니다 + 퇴사한 작업자로 표시됩니다. 작업 보고서에서 제외됩니다