feat: tkuser 통합 관리 서비스 + 전체 시스템 SSO 쿠키 인증 통합

- tkuser 서비스 신규 추가 (API + Web)
  - 사용자/권한/프로젝트/부서/작업자/작업장/설비/작업/휴가 통합 관리
  - 작업장 탭: 공장→작업장 드릴다운 네비게이션 + 구역지도 클릭 연동
  - 작업 탭: 공정(work_types)→작업(tasks) 계층 관리
  - 휴가 탭: 유형 관리 + 연차 배정(근로기준법 자동계산)
- 전 시스템 SSO 쿠키 인증으로 통합 (.technicalkorea.net 공유)
- System 2: 작업 이슈 리포트 기능 강화
- System 3: tkuser API 연동, 페이지 권한 체계 적용
- docker-compose에 tkuser-api, tkuser-web 서비스 추가
- ARCHITECTURE.md, DEPLOYMENT.md 문서 작성

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Hyungi Ahn
2026-02-12 13:45:52 +09:00
parent 6495b8af32
commit 733bb0cb35
96 changed files with 9721 additions and 825 deletions

View File

@@ -0,0 +1,66 @@
/**
* Department Controller
*
* 부서 CRUD
*/
const departmentModel = require('../models/departmentModel');
async function getAll(req, res, next) {
try {
const departments = await departmentModel.getAll();
res.json({ success: true, data: departments });
} catch (err) {
next(err);
}
}
async function getById(req, res, next) {
try {
const dept = await departmentModel.getById(parseInt(req.params.id));
if (!dept) {
return res.status(404).json({ success: false, error: '부서를 찾을 수 없습니다' });
}
res.json({ success: true, data: dept });
} catch (err) {
next(err);
}
}
async function create(req, res, next) {
try {
const { department_name } = req.body;
if (!department_name) {
return res.status(400).json({ success: false, error: '부서명은 필수입니다' });
}
const dept = await departmentModel.create(req.body);
res.status(201).json({ success: true, data: dept });
} catch (err) {
next(err);
}
}
async function update(req, res, next) {
try {
const id = parseInt(req.params.id);
const dept = await departmentModel.update(id, req.body);
if (!dept) {
return res.status(404).json({ success: false, error: '부서를 찾을 수 없습니다' });
}
res.json({ success: true, data: dept });
} catch (err) {
next(err);
}
}
async function remove(req, res, next) {
try {
const id = parseInt(req.params.id);
await departmentModel.deactivate(id);
res.json({ success: true, message: '부서가 비활성화되었습니다' });
} catch (err) {
next(err);
}
}
module.exports = { getAll, getById, create, update, remove };

View File

@@ -0,0 +1,141 @@
/**
* Equipment Controller
*
* 설비 CRUD + 지도위치 + 사진
*/
const equipmentModel = require('../models/equipmentModel');
const path = require('path');
const fs = require('fs');
// ==================== 기본 CRUD ====================
async function getAll(req, res, next) {
try {
const filters = {};
if (req.query.workplace_id) filters.workplace_id = parseInt(req.query.workplace_id);
if (req.query.equipment_type) filters.equipment_type = req.query.equipment_type;
if (req.query.status) filters.status = req.query.status;
if (req.query.search) filters.search = req.query.search;
const equipments = await equipmentModel.getAll(filters);
res.json({ success: true, data: equipments });
} catch (err) { next(err); }
}
async function getById(req, res, next) {
try {
const eq = await equipmentModel.getById(parseInt(req.params.id));
if (!eq) return res.status(404).json({ success: false, error: '설비를 찾을 수 없습니다' });
res.json({ success: true, data: eq });
} catch (err) { next(err); }
}
async function getByWorkplace(req, res, next) {
try {
const equipments = await equipmentModel.getByWorkplace(parseInt(req.params.workplaceId));
res.json({ success: true, data: equipments });
} catch (err) { next(err); }
}
async function create(req, res, next) {
try {
const { equipment_code, equipment_name } = req.body;
if (!equipment_code || !equipment_name) return res.status(400).json({ success: false, error: '관리번호와 설비명은 필수입니다' });
const dup = await equipmentModel.checkDuplicateCode(equipment_code);
if (dup) return res.status(409).json({ success: false, error: '이미 존재하는 관리번호입니다' });
const eq = await equipmentModel.create(req.body);
res.status(201).json({ success: true, data: eq });
} catch (err) { next(err); }
}
async function update(req, res, next) {
try {
const id = parseInt(req.params.id);
if (req.body.equipment_code) {
const dup = await equipmentModel.checkDuplicateCode(req.body.equipment_code, id);
if (dup) return res.status(409).json({ success: false, error: '이미 존재하는 관리번호입니다' });
}
const eq = await equipmentModel.update(id, req.body);
if (!eq) return res.status(404).json({ success: false, error: '설비를 찾을 수 없습니다' });
res.json({ success: true, data: eq });
} catch (err) { next(err); }
}
async function remove(req, res, next) {
try {
await equipmentModel.remove(parseInt(req.params.id));
res.json({ success: true, message: '설비가 삭제되었습니다' });
} catch (err) { next(err); }
}
async function getTypes(req, res, next) {
try {
const types = await equipmentModel.getEquipmentTypes();
res.json({ success: true, data: types });
} catch (err) { next(err); }
}
async function getNextCode(req, res, next) {
try {
const code = await equipmentModel.getNextCode(req.query.prefix || 'TKP');
res.json({ success: true, data: code });
} catch (err) { next(err); }
}
// ==================== 지도 위치 ====================
async function updateMapPosition(req, res, next) {
try {
const id = parseInt(req.params.id);
const positionData = {
map_x_percent: req.body.map_x_percent,
map_y_percent: req.body.map_y_percent,
map_width_percent: req.body.map_width_percent,
map_height_percent: req.body.map_height_percent
};
if (req.body.workplace_id !== undefined) positionData.workplace_id = req.body.workplace_id;
const eq = await equipmentModel.updateMapPosition(id, positionData);
res.json({ success: true, data: eq });
} catch (err) { next(err); }
}
// ==================== 사진 ====================
async function addPhoto(req, res, next) {
try {
const equipmentId = parseInt(req.params.id);
if (!req.file) return res.status(400).json({ success: false, error: '사진 파일이 필요합니다' });
const photoData = {
photo_path: `/uploads/${req.file.filename}`,
description: req.body.description || null,
display_order: parseInt(req.body.display_order) || 0,
uploaded_by: req.user?.user_id || null
};
const result = await equipmentModel.addPhoto(equipmentId, photoData);
res.status(201).json({ success: true, data: result });
} catch (err) { next(err); }
}
async function getPhotos(req, res, next) {
try {
const results = await equipmentModel.getPhotos(parseInt(req.params.id));
res.json({ success: true, data: results });
} catch (err) { next(err); }
}
async function deletePhoto(req, res, next) {
try {
const result = await equipmentModel.deletePhoto(parseInt(req.params.photoId));
if (result.photo_path) {
const filePath = path.join(__dirname, '..', result.photo_path);
fs.unlink(filePath, () => {});
}
res.json({ success: true, message: '사진이 삭제되었습니다' });
} catch (err) { next(err); }
}
module.exports = {
getAll, getById, getByWorkplace, create, update, remove, getTypes, getNextCode,
updateMapPosition,
addPhoto, getPhotos, deletePhoto
};

View File

@@ -0,0 +1,159 @@
/**
* Permission Controller
*
* 페이지 권한 관리 (system3 page_permissions.py 포팅)
*/
const permissionModel = require('../models/permissionModel');
const userModel = require('../models/userModel');
/**
* GET /api/users/:id/page-permissions - 사용자 권한 조회
*/
async function getUserPermissions(req, res, next) {
try {
const userId = parseInt(req.params.id);
const requesterId = req.user.user_id || req.user.id;
// 관리자이거나 본인만 조회 가능
if (req.user.role !== 'admin' && requesterId !== userId) {
return res.status(403).json({ success: false, error: '권한이 없습니다' });
}
const user = await userModel.findById(userId);
if (!user) {
return res.status(404).json({ success: false, error: '사용자를 찾을 수 없습니다' });
}
const permissions = await permissionModel.getUserPermissions(userId);
res.json(permissions);
} catch (err) {
next(err);
}
}
/**
* POST /api/permissions/grant - 단건 권한 부여
*/
async function grantPermission(req, res, next) {
try {
const { user_id, page_name, can_access, notes } = req.body;
const grantedById = req.user.user_id || req.user.id;
// 대상 사용자 확인
const targetUser = await userModel.findById(user_id);
if (!targetUser) {
return res.status(404).json({ success: false, error: '사용자를 찾을 수 없습니다' });
}
// 유효한 페이지명 확인
if (!permissionModel.DEFAULT_PAGES[page_name]) {
return res.status(400).json({ success: false, error: '유효하지 않은 페이지명입니다' });
}
const result = await permissionModel.grantPermission({
user_id,
page_name,
can_access,
granted_by_id: grantedById,
notes
});
res.json({ success: true, message: '권한이 설정되었습니다', data: result });
} catch (err) {
next(err);
}
}
/**
* POST /api/permissions/bulk-grant - 일괄 권한 부여
*/
async function bulkGrant(req, res, next) {
try {
const { user_id, permissions } = req.body;
const grantedById = req.user.user_id || req.user.id;
// 대상 사용자 확인
const targetUser = await userModel.findById(user_id);
if (!targetUser) {
return res.status(404).json({ success: false, error: '사용자를 찾을 수 없습니다' });
}
const result = await permissionModel.bulkGrant({
user_id,
permissions,
granted_by_id: grantedById
});
res.json({
success: true,
message: `${result.updated_count}개의 권한이 설정되었습니다`,
updated_count: result.updated_count
});
} catch (err) {
next(err);
}
}
/**
* GET /api/permissions/check/:uid/:page - 접근 권한 확인
*/
async function checkAccess(req, res, next) {
try {
const userId = parseInt(req.params.uid);
const pageName = req.params.page;
// 사용자 확인
const user = await userModel.findById(userId);
if (!user) {
return res.status(404).json({ success: false, error: '사용자를 찾을 수 없습니다' });
}
// admin은 모든 페이지 접근 가능
if (user.role === 'admin') {
return res.json({ can_access: true, reason: 'admin_role' });
}
const result = await permissionModel.checkAccess(userId, pageName);
res.json(result);
} catch (err) {
next(err);
}
}
/**
* GET /api/permissions/available-pages - 설정 가능 페이지 목록
*/
async function getAvailablePages(req, res) {
res.json({
pages: permissionModel.DEFAULT_PAGES,
total_count: Object.keys(permissionModel.DEFAULT_PAGES).length
});
}
/**
* DELETE /api/permissions/:id - 권한 삭제
*/
async function deletePermission(req, res, next) {
try {
const permissionId = parseInt(req.params.id);
const deleted = await permissionModel.deletePermission(permissionId);
if (!deleted) {
return res.status(404).json({ success: false, error: '권한을 찾을 수 없습니다' });
}
res.json({ success: true, message: '권한이 삭제되었습니다. 기본값이 적용됩니다.' });
} catch (err) {
next(err);
}
}
module.exports = {
getUserPermissions,
grantPermission,
bulkGrant,
checkAccess,
getAvailablePages,
deletePermission
};

View File

@@ -0,0 +1,83 @@
/**
* Project Controller
*
* 프로젝트 CRUD
*/
const projectModel = require('../models/projectModel');
async function getAll(req, res, next) {
try {
const projects = await projectModel.getAll();
res.json({ success: true, data: projects });
} catch (err) {
next(err);
}
}
async function getActive(req, res, next) {
try {
const projects = await projectModel.getActive();
res.json({ success: true, data: projects });
} catch (err) {
next(err);
}
}
async function getById(req, res, next) {
try {
const project = await projectModel.getById(parseInt(req.params.id));
if (!project) {
return res.status(404).json({ success: false, error: '프로젝트를 찾을 수 없습니다' });
}
res.json({ success: true, data: project });
} catch (err) {
next(err);
}
}
async function create(req, res, next) {
try {
const { job_no, project_name } = req.body;
if (!job_no || !project_name) {
return res.status(400).json({ success: false, error: 'Job No와 프로젝트명은 필수입니다' });
}
const project = await projectModel.create(req.body);
res.status(201).json({ success: true, data: project });
} catch (err) {
if (err.code === 'ER_DUP_ENTRY') {
return res.status(409).json({ success: false, error: '이미 존재하는 Job No입니다' });
}
next(err);
}
}
async function update(req, res, next) {
try {
const id = parseInt(req.params.id);
const project = await projectModel.update(id, req.body);
if (!project) {
return res.status(404).json({ success: false, error: '프로젝트를 찾을 수 없습니다' });
}
res.json({ success: true, data: project });
} catch (err) {
if (err.code === 'ER_DUP_ENTRY') {
return res.status(409).json({ success: false, error: '이미 존재하는 Job No입니다' });
}
next(err);
}
}
async function remove(req, res, next) {
try {
const id = parseInt(req.params.id);
await projectModel.deactivate(id);
res.json({ success: true, message: '프로젝트가 비활성화되었습니다' });
} catch (err) {
next(err);
}
}
module.exports = { getAll, getActive, getById, create, update, remove };

View File

@@ -0,0 +1,94 @@
/**
* Task Controller
*
* 공정(work_types) + 작업(tasks) CRUD
*/
const taskModel = require('../models/taskModel');
/* ===== Work Types ===== */
async function getWorkTypes(req, res, next) {
try {
const data = await taskModel.getWorkTypes();
res.json({ success: true, data });
} catch (err) { next(err); }
}
async function createWorkType(req, res, next) {
try {
const { name } = req.body;
if (!name) return res.status(400).json({ success: false, error: '공정명은 필수입니다' });
const data = await taskModel.createWorkType(req.body);
res.status(201).json({ success: true, data });
} catch (err) { next(err); }
}
async function updateWorkType(req, res, next) {
try {
const data = await taskModel.updateWorkType(parseInt(req.params.id), req.body);
if (!data) return res.status(404).json({ success: false, error: '공정을 찾을 수 없습니다' });
res.json({ success: true, data });
} catch (err) { next(err); }
}
async function deleteWorkType(req, res, next) {
try {
await taskModel.deleteWorkType(parseInt(req.params.id));
res.json({ success: true, message: '공정이 삭제되었습니다' });
} catch (err) { next(err); }
}
/* ===== Tasks ===== */
async function getTasks(req, res, next) {
try {
const workTypeId = req.query.work_type_id ? parseInt(req.query.work_type_id) : null;
const data = await taskModel.getTasks(workTypeId);
res.json({ success: true, data });
} catch (err) { next(err); }
}
async function getActiveTasks(req, res, next) {
try {
const data = await taskModel.getActiveTasks();
res.json({ success: true, data });
} catch (err) { next(err); }
}
async function getTaskById(req, res, next) {
try {
const data = await taskModel.getTaskById(parseInt(req.params.id));
if (!data) return res.status(404).json({ success: false, error: '작업을 찾을 수 없습니다' });
res.json({ success: true, data });
} catch (err) { next(err); }
}
async function createTask(req, res, next) {
try {
const { task_name } = req.body;
if (!task_name) return res.status(400).json({ success: false, error: '작업명은 필수입니다' });
const data = await taskModel.createTask(req.body);
res.status(201).json({ success: true, data });
} catch (err) { next(err); }
}
async function updateTask(req, res, next) {
try {
const data = await taskModel.updateTask(parseInt(req.params.id), req.body);
if (!data) return res.status(404).json({ success: false, error: '작업을 찾을 수 없습니다' });
res.json({ success: true, data });
} catch (err) { next(err); }
}
async function deleteTask(req, res, next) {
try {
await taskModel.deleteTask(parseInt(req.params.id));
res.json({ success: true, message: '작업이 삭제되었습니다' });
} catch (err) { next(err); }
}
module.exports = {
getWorkTypes, createWorkType, updateWorkType, deleteWorkType,
getTasks, getActiveTasks, getTaskById, createTask, updateTask, deleteTask
};

View File

@@ -0,0 +1,146 @@
/**
* User Controller
*
* 사용자 CRUD + 비밀번호 관리
*/
const userModel = require('../models/userModel');
/**
* GET /api/users - 전체 사용자 목록
*/
async function getUsers(req, res, next) {
try {
const users = await userModel.findAll();
res.json({ success: true, data: users });
} catch (err) {
next(err);
}
}
/**
* POST /api/users - 사용자 생성
*/
async function createUser(req, res, next) {
try {
const { username, password, name, full_name, department, role } = req.body;
if (!username || !password) {
return res.status(400).json({ success: false, error: '사용자명과 비밀번호는 필수입니다' });
}
const existing = await userModel.findByUsername(username);
if (existing) {
return res.status(409).json({ success: false, error: '이미 존재하는 사용자명입니다' });
}
const user = await userModel.create({
username,
password,
name: name || full_name,
department,
role
});
res.status(201).json({ success: true, data: user });
} catch (err) {
next(err);
}
}
/**
* PUT /api/users/:id - 사용자 수정
*/
async function updateUser(req, res, next) {
try {
const userId = parseInt(req.params.id);
const data = { ...req.body };
// full_name → name 매핑
if (data.full_name !== undefined && data.name === undefined) {
data.name = data.full_name;
delete data.full_name;
}
const user = await userModel.update(userId, data);
if (!user) {
return res.status(404).json({ success: false, error: '사용자를 찾을 수 없습니다' });
}
res.json({ success: true, data: user });
} catch (err) {
next(err);
}
}
/**
* DELETE /api/users/:id - 사용자 비활성화
*/
async function deleteUser(req, res, next) {
try {
const userId = parseInt(req.params.id);
await userModel.deleteUser(userId);
res.json({ success: true, message: '사용자가 비활성화되었습니다' });
} catch (err) {
next(err);
}
}
/**
* POST /api/users/:id/reset-password - 비밀번호 초기화 (admin)
*/
async function resetPassword(req, res, next) {
try {
const userId = parseInt(req.params.id);
const { new_password } = req.body;
const password = new_password || '000000';
const user = await userModel.update(userId, { password });
if (!user) {
return res.status(404).json({ success: false, error: '사용자를 찾을 수 없습니다' });
}
res.json({ success: true, message: '비밀번호가 초기화되었습니다' });
} catch (err) {
next(err);
}
}
/**
* POST /api/users/change-password - 본인 비밀번호 변경
*/
async function changePassword(req, res, next) {
try {
const { current_password, new_password } = req.body;
const userId = req.user.user_id || req.user.id;
if (!current_password || !new_password) {
return res.status(400).json({ success: false, error: '현재 비밀번호와 새 비밀번호를 입력하세요' });
}
if (new_password.length < 6) {
return res.status(400).json({ success: false, error: '새 비밀번호는 최소 6자 이상이어야 합니다' });
}
const user = await userModel.findById(userId);
if (!user) {
return res.status(404).json({ success: false, error: '사용자를 찾을 수 없습니다' });
}
const valid = await userModel.verifyPassword(current_password, user.password_hash);
if (!valid) {
return res.status(401).json({ success: false, error: '현재 비밀번호가 올바르지 않습니다' });
}
await userModel.update(userId, { password: new_password });
res.json({ success: true, message: '비밀번호가 변경되었습니다' });
} catch (err) {
next(err);
}
}
module.exports = {
getUsers,
createUser,
updateUser,
deleteUser,
resetPassword,
changePassword
};

View File

@@ -0,0 +1,126 @@
/**
* Vacation Controller
*
* 휴가 유형 + 연차 배정 관리
*/
const vacationModel = require('../models/vacationModel');
/* ===== Vacation Types ===== */
async function getVacationTypes(req, res, next) {
try {
const all = req.query.all === 'true';
const data = all ? await vacationModel.getAllVacationTypes() : await vacationModel.getVacationTypes();
res.json({ success: true, data });
} catch (err) { next(err); }
}
async function createVacationType(req, res, next) {
try {
const { type_code, type_name } = req.body;
if (!type_code || !type_name) return res.status(400).json({ success: false, error: '유형 코드와 이름은 필수입니다' });
const data = await vacationModel.createVacationType(req.body);
res.status(201).json({ success: true, data });
} catch (err) {
if (err.code === 'ER_DUP_ENTRY') return res.status(409).json({ success: false, error: '이미 존재하는 유형 코드입니다' });
next(err);
}
}
async function updateVacationType(req, res, next) {
try {
const data = await vacationModel.updateVacationType(parseInt(req.params.id), req.body);
if (!data) return res.status(404).json({ success: false, error: '휴가 유형을 찾을 수 없습니다' });
res.json({ success: true, data });
} catch (err) {
if (err.code === 'ER_DUP_ENTRY') return res.status(409).json({ success: false, error: '이미 존재하는 유형 코드입니다' });
next(err);
}
}
async function deleteVacationType(req, res, next) {
try {
await vacationModel.deleteVacationType(parseInt(req.params.id));
res.json({ success: true, message: '휴가 유형이 비활성화되었습니다' });
} catch (err) { next(err); }
}
async function updatePriorities(req, res, next) {
try {
const { items } = req.body;
if (!items || !Array.isArray(items)) return res.status(400).json({ success: false, error: 'items 배열이 필요합니다' });
await vacationModel.updatePriorities(items);
res.json({ success: true, message: '우선순위가 업데이트되었습니다' });
} catch (err) { next(err); }
}
/* ===== Vacation Balances ===== */
async function getBalancesByYear(req, res, next) {
try {
const year = parseInt(req.params.year);
const data = await vacationModel.getBalancesByYear(year);
res.json({ success: true, data });
} catch (err) { next(err); }
}
async function getBalancesByWorkerYear(req, res, next) {
try {
const workerId = parseInt(req.params.workerId);
const year = parseInt(req.params.year);
const data = await vacationModel.getBalancesByWorkerYear(workerId, year);
res.json({ success: true, data });
} catch (err) { next(err); }
}
async function createBalance(req, res, next) {
try {
const { worker_id, vacation_type_id, year } = req.body;
if (!worker_id || !vacation_type_id || !year) {
return res.status(400).json({ success: false, error: '작업자, 휴가유형, 연도는 필수입니다' });
}
const data = await vacationModel.createBalance({ ...req.body, created_by: req.user.user_id });
res.status(201).json({ success: true, data });
} catch (err) { next(err); }
}
async function updateBalance(req, res, next) {
try {
const data = await vacationModel.updateBalance(parseInt(req.params.id), req.body);
if (!data) return res.status(404).json({ success: false, error: '배정 정보를 찾을 수 없습니다' });
res.json({ success: true, data });
} catch (err) { next(err); }
}
async function deleteBalance(req, res, next) {
try {
await vacationModel.deleteBalance(parseInt(req.params.id));
res.json({ success: true, message: '삭제되었습니다' });
} catch (err) { next(err); }
}
async function bulkUpsertBalances(req, res, next) {
try {
const { balances } = req.body;
if (!balances || !Array.isArray(balances)) return res.status(400).json({ success: false, error: 'balances 배열이 필요합니다' });
const items = balances.map(b => ({ ...b, created_by: req.user.user_id }));
const count = await vacationModel.bulkUpsertBalances(items);
res.json({ success: true, data: { count }, message: `${count}건 처리되었습니다` });
} catch (err) { next(err); }
}
async function autoCalculate(req, res, next) {
try {
const { year } = req.body;
if (!year) return res.status(400).json({ success: false, error: '연도는 필수입니다' });
const result = await vacationModel.autoCalculateForAllWorkers(year, req.user.user_id);
res.json({ success: true, data: result, message: `${result.count}명 자동 배정 완료` });
} catch (err) { next(err); }
}
module.exports = {
getVacationTypes, createVacationType, updateVacationType, deleteVacationType, updatePriorities,
getBalancesByYear, getBalancesByWorkerYear, createBalance, updateBalance, deleteBalance,
bulkUpsertBalances, autoCalculate
};

View File

@@ -0,0 +1,66 @@
/**
* Worker Controller
*
* 작업자 CRUD
*/
const workerModel = require('../models/workerModel');
async function getAll(req, res, next) {
try {
const workers = await workerModel.getAll();
res.json({ success: true, data: workers });
} catch (err) {
next(err);
}
}
async function getById(req, res, next) {
try {
const worker = await workerModel.getById(parseInt(req.params.id));
if (!worker) {
return res.status(404).json({ success: false, error: '작업자를 찾을 수 없습니다' });
}
res.json({ success: true, data: worker });
} catch (err) {
next(err);
}
}
async function create(req, res, next) {
try {
const { worker_name } = req.body;
if (!worker_name) {
return res.status(400).json({ success: false, error: '작업자 이름은 필수입니다' });
}
const worker = await workerModel.create(req.body);
res.status(201).json({ success: true, data: worker });
} catch (err) {
next(err);
}
}
async function update(req, res, next) {
try {
const id = parseInt(req.params.id);
const worker = await workerModel.update(id, req.body);
if (!worker) {
return res.status(404).json({ success: false, error: '작업자를 찾을 수 없습니다' });
}
res.json({ success: true, data: worker });
} catch (err) {
next(err);
}
}
async function remove(req, res, next) {
try {
const id = parseInt(req.params.id);
await workerModel.deactivate(id);
res.json({ success: true, message: '작업자가 비활성화되었습니다' });
} catch (err) {
next(err);
}
}
module.exports = { getAll, getById, create, update, remove };

View File

@@ -0,0 +1,155 @@
/**
* Workplace Controller
*
* 작업장 CRUD + 카테고리 조회
*/
const workplaceModel = require('../models/workplaceModel');
async function getAll(req, res, next) {
try {
const workplaces = await workplaceModel.getAll();
res.json({ success: true, data: workplaces });
} catch (err) {
next(err);
}
}
async function getById(req, res, next) {
try {
const wp = await workplaceModel.getById(parseInt(req.params.id));
if (!wp) {
return res.status(404).json({ success: false, error: '작업장을 찾을 수 없습니다' });
}
res.json({ success: true, data: wp });
} catch (err) {
next(err);
}
}
async function getCategories(req, res, next) {
try {
const categories = await workplaceModel.getCategories();
res.json({ success: true, data: categories });
} catch (err) {
next(err);
}
}
async function create(req, res, next) {
try {
const { workplace_name } = req.body;
if (!workplace_name) {
return res.status(400).json({ success: false, error: '작업장명은 필수입니다' });
}
const wp = await workplaceModel.create(req.body);
res.status(201).json({ success: true, data: wp });
} catch (err) {
next(err);
}
}
async function update(req, res, next) {
try {
const id = parseInt(req.params.id);
const wp = await workplaceModel.update(id, req.body);
if (!wp) {
return res.status(404).json({ success: false, error: '작업장을 찾을 수 없습니다' });
}
res.json({ success: true, data: wp });
} catch (err) {
next(err);
}
}
async function remove(req, res, next) {
try {
const id = parseInt(req.params.id);
await workplaceModel.deactivate(id);
res.json({ success: true, message: '작업장이 비활성화되었습니다' });
} catch (err) {
next(err);
}
}
// ==================== 구역지도 ====================
async function uploadCategoryLayoutImage(req, res, next) {
try {
if (!req.file) {
return res.status(400).json({ success: false, error: '이미지 파일이 필요합니다' });
}
const id = parseInt(req.params.id);
const imagePath = `/uploads/${req.file.filename}`;
const category = await workplaceModel.updateCategoryLayoutImage(id, imagePath);
if (!category) {
return res.status(404).json({ success: false, error: '카테고리를 찾을 수 없습니다' });
}
res.json({ success: true, data: { image_path: imagePath, category } });
} catch (err) {
next(err);
}
}
async function createMapRegion(req, res, next) {
try {
const { workplace_id, category_id } = req.body;
if (!workplace_id || !category_id) {
return res.status(400).json({ success: false, error: 'workplace_id와 category_id는 필수입니다' });
}
const region = await workplaceModel.createMapRegion(req.body);
res.status(201).json({ success: true, data: region });
} catch (err) {
next(err);
}
}
async function getMapRegionsByCategory(req, res, next) {
try {
const categoryId = parseInt(req.params.categoryId);
const regions = await workplaceModel.getMapRegionsByCategory(categoryId);
res.json({ success: true, data: regions });
} catch (err) {
next(err);
}
}
async function updateMapRegion(req, res, next) {
try {
const regionId = parseInt(req.params.id);
const region = await workplaceModel.updateMapRegion(regionId, req.body);
if (!region) {
return res.status(404).json({ success: false, error: '영역을 찾을 수 없습니다' });
}
res.json({ success: true, data: region });
} catch (err) {
next(err);
}
}
async function deleteMapRegion(req, res, next) {
try {
const regionId = parseInt(req.params.id);
await workplaceModel.deleteMapRegion(regionId);
res.json({ success: true, message: '영역이 삭제되었습니다' });
} catch (err) {
next(err);
}
}
async function uploadWorkplaceLayoutImage(req, res, next) {
try {
if (!req.file) return res.status(400).json({ success: false, error: '이미지 파일이 필요합니다' });
const id = parseInt(req.params.id);
const imagePath = `/uploads/${req.file.filename}`;
const wp = await workplaceModel.updateWorkplaceLayoutImage(id, imagePath);
if (!wp) return res.status(404).json({ success: false, error: '작업장을 찾을 수 없습니다' });
res.json({ success: true, data: { image_path: imagePath, workplace: wp } });
} catch (err) { next(err); }
}
module.exports = {
getAll, getById, getCategories, create, update, remove,
uploadCategoryLayoutImage, uploadWorkplaceLayoutImage,
createMapRegion, getMapRegionsByCategory, updateMapRegion, deleteMapRegion
};