refactor: System2/3, User Management SSO 인증 통합

- System2 신고: SSO JWT 인증 전환, API base 정리
- System3 부적합: SSO 인증 매니저 통합, 권한 체계 정비
- User Management: SSO 토큰 기반 사용자 관리 API 연동

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Hyungi Ahn
2026-03-06 23:18:23 +09:00
parent 61c810bd47
commit 11cffbd920
26 changed files with 528 additions and 1824 deletions

View File

@@ -8,209 +8,136 @@ const mProjectService = require('../services/mProjectService');
// ==================== 신고 카테고리 관리 ====================
/**
* 모든 카테고리 조회
*/
exports.getAllCategories = (req, res) => {
workIssueModel.getAllCategories((err, categories) => {
if (err) {
console.error('카테고리 조회 실패:', err);
return res.status(500).json({ success: false, error: '카테고리 조회 실패' });
}
exports.getAllCategories = async (req, res) => {
try {
const categories = await workIssueModel.getAllCategories();
res.json({ success: true, data: categories });
});
} catch (err) {
console.error('카테고리 조회 실패:', err);
res.status(500).json({ success: false, error: '카테고리 조회 실패' });
}
};
/**
* 타입별 카테고리 조회
*/
exports.getCategoriesByType = (req, res) => {
exports.getCategoriesByType = async (req, res) => {
try {
const { type } = req.params;
if (!['nonconformity', 'safety', 'facility'].includes(type)) {
return res.status(400).json({ success: false, error: '유효하지 않은 카테고리 타입입니다.' });
}
workIssueModel.getCategoriesByType(type, (err, categories) => {
if (err) {
console.error('카테고리 조회 실패:', err);
return res.status(500).json({ success: false, error: '카테고리 조회 실패' });
}
const categories = await workIssueModel.getCategoriesByType(type);
res.json({ success: true, data: categories });
});
} catch (err) {
console.error('카테고리 조회 실패:', err);
res.status(500).json({ success: false, error: '카테고리 조회 실패' });
}
};
/**
* 카테고리 생성
*/
exports.createCategory = (req, res) => {
exports.createCategory = async (req, res) => {
try {
const { category_type, category_name, description, display_order } = req.body;
if (!category_type || !category_name) {
return res.status(400).json({ success: false, error: '카테고리 타입과 이름은 필수입니다.' });
}
workIssueModel.createCategory(
{ category_type, category_name, description, display_order },
(err, categoryId) => {
if (err) {
const categoryId = await workIssueModel.createCategory({ category_type, category_name, description, display_order });
res.status(201).json({ success: true, message: '카테고리가 생성되었습니다.', data: { category_id: categoryId } });
} catch (err) {
console.error('카테고리 생성 실패:', err);
return res.status(500).json({ success: false, error: '카테고리 생성 실패' });
res.status(500).json({ success: false, error: '카테고리 생성 실패' });
}
res.status(201).json({
success: true,
message: '카테고리가 생성되었습니다.',
data: { category_id: categoryId }
});
}
);
};
/**
* 카테고리 수정
*/
exports.updateCategory = (req, res) => {
exports.updateCategory = async (req, res) => {
try {
const { id } = req.params;
const { category_name, description, display_order, is_active } = req.body;
workIssueModel.updateCategory(
id,
{ category_name, description, display_order, is_active },
(err, result) => {
if (err) {
console.error('카테고리 수정 실패:', err);
return res.status(500).json({ success: false, error: '카테고리 수정 실패' });
}
await workIssueModel.updateCategory(id, { category_name, description, display_order, is_active });
res.json({ success: true, message: '카테고리가 수정되었습니다.' });
} catch (err) {
console.error('카테고리 수정 실패:', err);
res.status(500).json({ success: false, error: '카테고리 수정 실패' });
}
);
};
/**
* 카테고리 삭제
*/
exports.deleteCategory = (req, res) => {
exports.deleteCategory = async (req, res) => {
try {
const { id } = req.params;
workIssueModel.deleteCategory(id, (err, result) => {
if (err) {
console.error('카테고리 삭제 실패:', err);
return res.status(500).json({ success: false, error: '카테고리 삭제 실패' });
}
await workIssueModel.deleteCategory(id);
res.json({ success: true, message: '카테고리가 삭제되었습니다.' });
});
} catch (err) {
console.error('카테고리 삭제 실패:', err);
res.status(500).json({ success: false, error: '카테고리 삭제 실패' });
}
};
// ==================== 사전 정의 항목 관리 ====================
/**
* 카테고리별 항목 조회
*/
exports.getItemsByCategory = (req, res) => {
exports.getItemsByCategory = async (req, res) => {
try {
const { categoryId } = req.params;
workIssueModel.getItemsByCategory(categoryId, (err, items) => {
if (err) {
console.error('항목 조회 실패:', err);
return res.status(500).json({ success: false, error: '항목 조회 실패' });
}
const items = await workIssueModel.getItemsByCategory(categoryId);
res.json({ success: true, data: items });
});
} catch (err) {
console.error('항목 조회 실패:', err);
res.status(500).json({ success: false, error: '항목 조회 실패' });
}
};
/**
* 모든 항목 조회
*/
exports.getAllItems = (req, res) => {
workIssueModel.getAllItems((err, items) => {
if (err) {
console.error('항목 조회 실패:', err);
return res.status(500).json({ success: false, error: '항목 조회 실패' });
}
exports.getAllItems = async (req, res) => {
try {
const items = await workIssueModel.getAllItems();
res.json({ success: true, data: items });
});
} catch (err) {
console.error('항목 조회 실패:', err);
res.status(500).json({ success: false, error: '항목 조회 실패' });
}
};
/**
* 항목 생성
*/
exports.createItem = (req, res) => {
exports.createItem = async (req, res) => {
try {
const { category_id, item_name, description, severity, display_order } = req.body;
if (!category_id || !item_name) {
return res.status(400).json({ success: false, error: '카테고리 ID와 항목명은 필수입니다.' });
}
workIssueModel.createItem(
{ category_id, item_name, description, severity, display_order },
(err, itemId) => {
if (err) {
const itemId = await workIssueModel.createItem({ category_id, item_name, description, severity, display_order });
res.status(201).json({ success: true, message: '항목이 생성되었습니다.', data: { item_id: itemId } });
} catch (err) {
console.error('항목 생성 실패:', err);
return res.status(500).json({ success: false, error: '항목 생성 실패' });
res.status(500).json({ success: false, error: '항목 생성 실패' });
}
res.status(201).json({
success: true,
message: '항목이 생성되었습니다.',
data: { item_id: itemId }
});
}
);
};
/**
* 항목 수정
*/
exports.updateItem = (req, res) => {
exports.updateItem = async (req, res) => {
try {
const { id } = req.params;
const { item_name, description, severity, display_order, is_active } = req.body;
workIssueModel.updateItem(
id,
{ item_name, description, severity, display_order, is_active },
(err, result) => {
if (err) {
console.error('항목 수정 실패:', err);
return res.status(500).json({ success: false, error: '항목 수정 실패' });
}
await workIssueModel.updateItem(id, { item_name, description, severity, display_order, is_active });
res.json({ success: true, message: '항목이 수정되었습니다.' });
} catch (err) {
console.error('항목 수정 실패:', err);
res.status(500).json({ success: false, error: '항목 수정 실패' });
}
);
};
/**
* 항목 삭제
*/
exports.deleteItem = (req, res) => {
exports.deleteItem = async (req, res) => {
try {
const { id } = req.params;
workIssueModel.deleteItem(id, (err, result) => {
if (err) {
console.error('항목 삭제 실패:', err);
return res.status(500).json({ success: false, error: '항목 삭제 실패' });
}
await workIssueModel.deleteItem(id);
res.json({ success: true, message: '항목이 삭제되었습니다.' });
});
} catch (err) {
console.error('항목 삭제 실패:', err);
res.status(500).json({ success: false, error: '항목 삭제 실패' });
}
};
// ==================== 문제 신고 관리 ====================
/**
* 신고 생성
*/
exports.createReport = async (req, res) => {
try {
const {
factory_category_id,
workplace_id,
custom_location,
tbm_session_id,
visit_request_id,
issue_category_id,
issue_item_id,
custom_item_name, // 직접 입력한 항목명
project_id,
additional_description,
photos = []
factory_category_id, workplace_id, custom_location,
tbm_session_id, visit_request_id, issue_category_id,
issue_item_id, custom_item_name, project_id,
additional_description, photos = []
} = req.body;
const reporter_id = req.user.user_id;
@@ -218,73 +145,34 @@ exports.createReport = async (req, res) => {
if (!issue_category_id) {
return res.status(400).json({ success: false, error: '신고 카테고리는 필수입니다.' });
}
// 위치 정보 검증 (지도 선택 또는 기타 위치)
if (!factory_category_id && !custom_location) {
return res.status(400).json({ success: false, error: '위치 정보는 필수입니다.' });
}
// 항목 검증 (기존 항목 또는 직접 입력)
if (!issue_item_id && !custom_item_name) {
return res.status(400).json({ success: false, error: '신고 항목은 필수입니다.' });
}
// 직접 입력한 항목이 있으면 DB에 저장
let finalItemId = issue_item_id;
if (custom_item_name && !issue_item_id) {
try {
finalItemId = await new Promise((resolve, reject) => {
workIssueModel.createItem(
{
finalItemId = await workIssueModel.createItem({
category_id: issue_category_id,
item_name: custom_item_name,
description: '사용자 직접 입력',
severity: 'medium',
display_order: 999 // 마지막에 표시
},
(err, itemId) => {
if (err) reject(err);
else resolve(itemId);
}
);
display_order: 999
});
} catch (itemErr) {
console.error('커스텀 항목 생성 실패:', itemErr);
return res.status(500).json({ success: false, error: '항목 저장 실패' });
}
}
// 사진 저장 (최대 5장)
const photoPaths = {
photo_path1: null,
photo_path2: null,
photo_path3: null,
photo_path4: null,
photo_path5: null
};
const photoPaths = { photo_path1: null, photo_path2: null, photo_path3: null, photo_path4: null, photo_path5: null };
for (let i = 0; i < Math.min(photos.length, 5); i++) {
if (photos[i]) {
const savedPath = await imageUploadService.saveBase64Image(photos[i], 'issue');
if (savedPath) {
photoPaths[`photo_path${i + 1}`] = savedPath;
}
if (savedPath) photoPaths[`photo_path${i + 1}`] = savedPath;
}
}
// category_type 조회
let categoryType = null;
try {
const catInfo = await new Promise((resolve, reject) => {
workIssueModel.getCategoryById(issue_category_id, (catErr, data) => {
if (catErr) reject(catErr); else resolve(data);
});
});
if (catInfo) categoryType = catInfo.category_type;
} catch (catErr) {
console.error('카테고리 조회 실패:', catErr);
return res.status(500).json({ success: false, error: '카테고리 정보 조회 실패' });
}
const catInfo = await workIssueModel.getCategoryById(issue_category_id);
const categoryType = catInfo ? catInfo.category_type : null;
const reportData = {
reporter_id,
@@ -301,13 +189,8 @@ exports.createReport = async (req, res) => {
...photoPaths
};
workIssueModel.createReport(reportData, async (err, reportId) => {
if (err) {
console.error('신고 생성 실패:', err);
return res.status(500).json({ success: false, error: '신고 생성 실패' });
}
const reportId = await workIssueModel.createReport(reportData);
// 응답 먼저 반환 (사용자 대기 X)
res.status(201).json({
success: true,
message: '문제 신고가 등록되었습니다.',
@@ -316,14 +199,7 @@ exports.createReport = async (req, res) => {
// 부적합 유형이면 System 3(tkqc)으로 비동기 전달
try {
const categoryInfo = await new Promise((resolve, reject) => {
workIssueModel.getCategoryById(issue_category_id, (catErr, data) => {
if (catErr) reject(catErr); else resolve(data);
});
});
if (categoryInfo && categoryInfo.category_type === 'nonconformity') {
// 저장된 사진 파일을 base64로 읽어서 System 3에 전달
if (catInfo && catInfo.category_type === 'nonconformity') {
const fs = require('fs').promises;
const path = require('path');
const photoBase64List = [];
@@ -332,41 +208,29 @@ exports.createReport = async (req, res) => {
try {
const filePath = path.join(__dirname, '..', p);
const buf = await fs.readFile(filePath);
const b64 = `data:image/jpeg;base64,${buf.toString('base64')}`;
photoBase64List.push(b64);
photoBase64List.push(`data:image/jpeg;base64,${buf.toString('base64')}`);
} catch (readErr) {
console.error('사진 파일 읽기 실패:', p, readErr.message);
}
}
const descText = additional_description || categoryInfo.category_name;
// 위치 정보 조회
let locationInfo = custom_location || null;
if (factory_category_id) {
try {
const locationParts = await new Promise((resolve, reject) => {
workIssueModel.getLocationNames(factory_category_id, workplace_id, (locErr, data) => {
if (locErr) reject(locErr); else resolve(data);
});
});
const locationParts = await workIssueModel.getLocationNames(factory_category_id, workplace_id);
if (locationParts) {
locationInfo = locationParts.factory_name || '';
if (locationParts.workplace_name) {
locationInfo += ` - ${locationParts.workplace_name}`;
}
if (locationParts.workplace_name) locationInfo += ` - ${locationParts.workplace_name}`;
}
} catch (locErr) {
console.error('위치 정보 조회 실패:', locErr.message);
}
}
// 원래 신고자의 SSO 토큰 추출
const originalToken = (req.headers['authorization'] || '').replace('Bearer ', '');
const result = await mProjectService.sendToMProject({
category: categoryInfo.category_name,
description: descText,
category: catInfo.category_name,
description: additional_description || catInfo.category_name,
reporter_name: req.user.name || req.user.username,
tk_issue_id: reportId,
project_id: project_id || null,
@@ -375,23 +239,20 @@ exports.createReport = async (req, res) => {
ssoToken: originalToken
});
if (result.success && result.mProjectId) {
workIssueModel.updateMProjectId(reportId, result.mProjectId, () => {});
await workIssueModel.updateMProjectId(reportId, result.mProjectId);
}
}
} catch (e) {
console.error('System3 연동 실패 (신고는 정상 저장됨):', e.message);
}
});
} catch (error) {
console.error('신고 생성 에러:', error);
res.status(500).json({ success: false, error: '서버 오류가 발생했습니다.' });
}
};
/**
* 신고 목록 조회
*/
exports.getAllReports = (req, res) => {
exports.getAllReports = async (req, res) => {
try {
const filters = {
status: req.query.status,
category_type: req.query.category_type,
@@ -406,38 +267,28 @@ exports.getAllReports = (req, res) => {
offset: req.query.offset
};
// 일반 사용자는 자신의 신고만 조회 (관리자 제외)
const userLevel = req.user.access_level;
if (!['admin', 'system', 'support_team'].includes(userLevel)) {
filters.reporter_id = req.user.user_id;
}
workIssueModel.getAllReports(filters, (err, reports) => {
if (err) {
console.error('신고 목록 조회 실패:', err);
return res.status(500).json({ success: false, error: '신고 목록 조회 실패' });
}
const reports = await workIssueModel.getAllReports(filters);
res.json({ success: true, data: reports });
});
} catch (err) {
console.error('신고 목록 조회 실패:', err);
res.status(500).json({ success: false, error: '신고 목록 조회 실패' });
}
};
/**
* 신고 상세 조회
*/
exports.getReportById = (req, res) => {
exports.getReportById = async (req, res) => {
try {
const { id } = req.params;
workIssueModel.getReportById(id, (err, report) => {
if (err) {
console.error('신고 상세 조회 실패:', err);
return res.status(500).json({ success: false, error: '신고 상세 조회 실패' });
}
const report = await workIssueModel.getReportById(id);
if (!report) {
return res.status(404).json({ success: false, error: '신고를 찾을 수 없습니다.' });
}
// 권한 확인: 본인, 담당자, 또는 관리자
const userLevel = req.user.access_level;
const isOwner = report.reporter_id === req.user.user_id;
const isAssignee = report.assigned_user_id === req.user.user_id;
@@ -448,28 +299,21 @@ exports.getReportById = (req, res) => {
}
res.json({ success: true, data: report });
});
} catch (err) {
console.error('신고 상세 조회 실패:', err);
res.status(500).json({ success: false, error: '신고 상세 조회 실패' });
}
};
/**
* 신고 수정
*/
exports.updateReport = async (req, res) => {
try {
const { id } = req.params;
// 기존 신고 확인
workIssueModel.getReportById(id, async (err, report) => {
if (err) {
console.error('신고 조회 실패:', err);
return res.status(500).json({ success: false, error: '신고 조회 실패' });
}
const report = await workIssueModel.getReportById(id);
if (!report) {
return res.status(404).json({ success: false, error: '신고를 찾을 수 없습니다.' });
}
// 권한 확인
const userLevel = req.user.access_level;
const isOwner = report.reporter_id === req.user.user_id;
const isManager = ['admin', 'system'].includes(userLevel);
@@ -477,80 +321,44 @@ exports.updateReport = async (req, res) => {
if (!isOwner && !isManager) {
return res.status(403).json({ success: false, error: '수정 권한이 없습니다.' });
}
// 상태 확인: reported 상태에서만 수정 가능 (관리자 제외)
if (!isManager && report.status !== 'reported') {
return res.status(400).json({ success: false, error: '이미 접수된 신고는 수정할 수 없습니다.' });
}
const {
factory_category_id,
workplace_id,
custom_location,
issue_category_id,
issue_item_id,
additional_description,
photos = []
} = req.body;
const { factory_category_id, workplace_id, custom_location, issue_category_id, issue_item_id, additional_description, photos = [] } = req.body;
// 사진 업데이트 처리
const photoPaths = {};
for (let i = 0; i < Math.min(photos.length, 5); i++) {
if (photos[i]) {
// 기존 사진 삭제
const oldPath = report[`photo_path${i + 1}`];
if (oldPath) {
await imageUploadService.deleteFile(oldPath);
}
// 새 사진 저장
if (oldPath) await imageUploadService.deleteFile(oldPath);
const savedPath = await imageUploadService.saveBase64Image(photos[i], 'issue');
if (savedPath) {
photoPaths[`photo_path${i + 1}`] = savedPath;
}
if (savedPath) photoPaths[`photo_path${i + 1}`] = savedPath;
}
}
const updateData = {
factory_category_id,
workplace_id,
custom_location,
issue_category_id,
issue_item_id,
additional_description,
await workIssueModel.updateReport(id, {
factory_category_id, workplace_id, custom_location,
issue_category_id, issue_item_id, additional_description,
...photoPaths
};
}, req.user.user_id);
workIssueModel.updateReport(id, updateData, req.user.user_id, (updateErr, result) => {
if (updateErr) {
console.error('신고 수정 실패:', updateErr);
return res.status(500).json({ success: false, error: '신고 수정 실패' });
}
res.json({ success: true, message: '신고가 수정되었습니다.' });
});
});
} catch (error) {
console.error('신고 수정 에러:', error);
res.status(500).json({ success: false, error: '서버 오류가 발생했습니다.' });
}
};
/**
* 신고 삭제
*/
exports.deleteReport = async (req, res) => {
try {
const { id } = req.params;
workIssueModel.getReportById(id, async (err, report) => {
if (err) {
console.error('신고 조회 실패:', err);
return res.status(500).json({ success: false, error: '신고 조회 실패' });
}
const report = await workIssueModel.getReportById(id);
if (!report) {
return res.status(404).json({ success: false, error: '신고를 찾을 수 없습니다.' });
}
// 권한 확인
const userLevel = req.user.access_level;
const isOwner = report.reporter_id === req.user.user_id;
const isManager = ['admin', 'system'].includes(userLevel);
@@ -559,13 +367,8 @@ exports.deleteReport = async (req, res) => {
return res.status(403).json({ success: false, error: '삭제 권한이 없습니다.' });
}
workIssueModel.deleteReport(id, async (deleteErr, { result, photos }) => {
if (deleteErr) {
console.error('신고 삭제 실패:', deleteErr);
return res.status(500).json({ success: false, error: '신고 삭제 실패' });
}
const { result, photos } = await workIssueModel.deleteReport(id);
// 사진 파일 삭제
if (photos) {
const allPhotos = [
photos.photo_path1, photos.photo_path2, photos.photo_path3,
@@ -576,212 +379,151 @@ exports.deleteReport = async (req, res) => {
}
res.json({ success: true, message: '신고가 삭제되었습니다.' });
});
});
} catch (err) {
console.error('신고 삭제 실패:', err);
res.status(500).json({ success: false, error: '신고 삭제 실패' });
}
};
// ==================== 상태 관리 ====================
/**
* 신고 접수
*/
exports.receiveReport = (req, res) => {
exports.receiveReport = async (req, res) => {
try {
const { id } = req.params;
workIssueModel.receiveReport(id, req.user.user_id, (err, result) => {
if (err) {
console.error('신고 접수 실패:', err);
return res.status(400).json({ success: false, error: err.message || '신고 접수 실패' });
}
await workIssueModel.receiveReport(id, req.user.user_id);
res.json({ success: true, message: '신고가 접수되었습니다.' });
});
} catch (err) {
console.error('신고 접수 실패:', err);
res.status(400).json({ success: false, error: err.message || '신고 접수 실패' });
}
};
/**
* 담당자 배정
*/
exports.assignReport = (req, res) => {
exports.assignReport = async (req, res) => {
try {
const { id } = req.params;
const { assigned_department, assigned_user_id } = req.body;
if (!assigned_user_id) {
return res.status(400).json({ success: false, error: '담당자는 필수입니다.' });
}
workIssueModel.assignReport(id, {
assigned_department,
assigned_user_id,
assigned_by: req.user.user_id
}, (err, result) => {
if (err) {
console.error('담당자 배정 실패:', err);
return res.status(400).json({ success: false, error: err.message || '담당자 배정 실패' });
}
await workIssueModel.assignReport(id, { assigned_department, assigned_user_id, assigned_by: req.user.user_id });
res.json({ success: true, message: '담당자가 배정되었습니다.' });
});
};
/**
* 처리 시작
*/
exports.startProcessing = (req, res) => {
const { id } = req.params;
workIssueModel.startProcessing(id, req.user.user_id, (err, result) => {
if (err) {
console.error('처리 시작 실패:', err);
return res.status(400).json({ success: false, error: err.message || '처리 시작 실패' });
} catch (err) {
console.error('담당자 배정 실패:', err);
res.status(400).json({ success: false, error: err.message || '담당자 배정 실패' });
}
};
exports.startProcessing = async (req, res) => {
try {
const { id } = req.params;
await workIssueModel.startProcessing(id, req.user.user_id);
res.json({ success: true, message: '처리가 시작되었습니다.' });
} catch (err) {
console.error('처리 시작 실패:', err);
res.status(400).json({ success: false, error: err.message || '처리 시작 실패' });
}
res.json({ success: true, message: '처리가 시작되었습니다.' });
});
};
/**
* 처리 완료
*/
exports.completeReport = async (req, res) => {
try {
const { id } = req.params;
const { resolution_notes, resolution_photos = [] } = req.body;
// 완료 사진 저장
let resolution_photo_path1 = null;
let resolution_photo_path2 = null;
if (resolution_photos[0]) resolution_photo_path1 = await imageUploadService.saveBase64Image(resolution_photos[0], 'resolution');
if (resolution_photos[1]) resolution_photo_path2 = await imageUploadService.saveBase64Image(resolution_photos[1], 'resolution');
if (resolution_photos[0]) {
resolution_photo_path1 = await imageUploadService.saveBase64Image(resolution_photos[0], 'resolution');
}
if (resolution_photos[1]) {
resolution_photo_path2 = await imageUploadService.saveBase64Image(resolution_photos[1], 'resolution');
}
workIssueModel.completeReport(id, {
resolution_notes,
resolution_photo_path1,
resolution_photo_path2,
resolved_by: req.user.user_id
}, (err, result) => {
if (err) {
console.error('처리 완료 실패:', err);
return res.status(400).json({ success: false, error: err.message || '처리 완료 실패' });
}
await workIssueModel.completeReport(id, { resolution_notes, resolution_photo_path1, resolution_photo_path2, resolved_by: req.user.user_id });
res.json({ success: true, message: '처리가 완료되었습니다.' });
});
} catch (error) {
console.error('처리 완료 에러:', error);
res.status(500).json({ success: false, error: '서버 오류가 발생했습니다.' });
} catch (err) {
console.error('처리 완료 실패:', err);
res.status(400).json({ success: false, error: err.message || '처리 완료 실패' });
}
};
/**
* 신고 종료
*/
exports.closeReport = (req, res) => {
exports.closeReport = async (req, res) => {
try {
const { id } = req.params;
workIssueModel.closeReport(id, req.user.user_id, (err, result) => {
if (err) {
console.error('신고 종료 실패:', err);
return res.status(400).json({ success: false, error: err.message || '신고 종료 실패' });
}
await workIssueModel.closeReport(id, req.user.user_id);
res.json({ success: true, message: '신고가 종료되었습니다.' });
});
} catch (err) {
console.error('신고 종료 실패:', err);
res.status(400).json({ success: false, error: err.message || '신고 종료 실패' });
}
};
/**
* 상태 변경 이력 조회
*/
exports.getStatusLogs = (req, res) => {
exports.getStatusLogs = async (req, res) => {
try {
const { id } = req.params;
workIssueModel.getStatusLogs(id, (err, logs) => {
if (err) {
console.error('상태 이력 조회 실패:', err);
return res.status(500).json({ success: false, error: '상태 이력 조회 실패' });
}
const logs = await workIssueModel.getStatusLogs(id);
res.json({ success: true, data: logs });
});
} catch (err) {
console.error('상태 이력 조회 실패:', err);
res.status(500).json({ success: false, error: '상태 이력 조회 실패' });
}
};
// ==================== 유형 이관 ====================
/**
* 신고 유형 이관
*/
exports.transferReport = (req, res) => {
exports.transferReport = async (req, res) => {
try {
const { id } = req.params;
const { category_type } = req.body;
// ENUM 유효성 검증
const validTypes = ['nonconformity', 'safety', 'facility'];
if (!category_type || !validTypes.includes(category_type)) {
return res.status(400).json({ success: false, error: '유효하지 않은 유형입니다. (nonconformity, safety, facility)' });
}
workIssueModel.transferCategoryType(id, category_type, req.user.user_id, (err, result) => {
if (err) {
console.error('유형 이관 실패:', err);
return res.status(400).json({ success: false, error: err.message || '유형 이관 실패' });
}
await workIssueModel.transferCategoryType(id, category_type, req.user.user_id);
res.json({ success: true, message: '유형이 이관되었습니다.' });
});
} catch (err) {
console.error('유형 이관 실패:', err);
res.status(400).json({ success: false, error: err.message || '유형 이관 실패' });
}
};
// ==================== 통계 ====================
/**
* 통계 요약
*/
exports.getStatsSummary = (req, res) => {
exports.getStatsSummary = async (req, res) => {
try {
const filters = {
category_type: req.query.category_type,
start_date: req.query.start_date,
end_date: req.query.end_date,
factory_category_id: req.query.factory_category_id
};
workIssueModel.getStatsSummary(filters, (err, stats) => {
if (err) {
const stats = await workIssueModel.getStatsSummary(filters);
res.json({ success: true, data: stats });
} catch (err) {
console.error('통계 조회 실패:', err);
return res.status(500).json({ success: false, error: '통계 조회 실패' });
res.status(500).json({ success: false, error: '통계 조회 실패' });
}
res.json({ success: true, data: stats });
});
};
/**
* 카테고리별 통계
*/
exports.getStatsByCategory = (req, res) => {
const filters = {
start_date: req.query.start_date,
end_date: req.query.end_date
};
workIssueModel.getStatsByCategory(filters, (err, stats) => {
if (err) {
exports.getStatsByCategory = async (req, res) => {
try {
const filters = { start_date: req.query.start_date, end_date: req.query.end_date };
const stats = await workIssueModel.getStatsByCategory(filters);
res.json({ success: true, data: stats });
} catch (err) {
console.error('카테고리별 통계 조회 실패:', err);
return res.status(500).json({ success: false, error: '통계 조회 실패' });
res.status(500).json({ success: false, error: '통계 조회 실패' });
}
res.json({ success: true, data: stats });
});
};
/**
* 작업장별 통계
*/
exports.getStatsByWorkplace = (req, res) => {
exports.getStatsByWorkplace = async (req, res) => {
try {
const filters = {
start_date: req.query.start_date,
end_date: req.query.end_date,
factory_category_id: req.query.factory_category_id
};
workIssueModel.getStatsByWorkplace(filters, (err, stats) => {
if (err) {
console.error('작업장별 통계 조회 실패:', err);
return res.status(500).json({ success: false, error: '통계 조회 실패' });
}
const stats = await workIssueModel.getStatsByWorkplace(filters);
res.json({ success: true, data: stats });
});
} catch (err) {
console.error('작업장별 통계 조회 실패:', err);
res.status(500).json({ success: false, error: '통계 조회 실패' });
}
};

View File

@@ -1,933 +0,0 @@
/**
* 작업 중 문제 신고 모델
* 부적합/안전 신고 관련 DB 쿼리
*/
const { getDb } = require('../dbPool');
// ==================== 신고 카테고리 관리 ====================
/**
* 모든 신고 카테고리 조회
*/
const getAllCategories = async (callback) => {
try {
const db = await getDb();
const [rows] = await db.query(
`SELECT category_id, category_type, category_name, description, display_order, is_active, created_at
FROM issue_report_categories
ORDER BY category_type, display_order, category_id`
);
callback(null, rows);
} catch (err) {
callback(err);
}
};
/**
* 타입별 활성 카테고리 조회 (nonconformity/safety)
*/
const getCategoriesByType = async (categoryType, callback) => {
try {
const db = await getDb();
const [rows] = await db.query(
`SELECT category_id, category_type, category_name, description, display_order
FROM issue_report_categories
WHERE category_type = ? AND is_active = TRUE
ORDER BY display_order, category_id`,
[categoryType]
);
callback(null, rows);
} catch (err) {
callback(err);
}
};
/**
* 카테고리 생성
*/
const createCategory = async (categoryData, callback) => {
try {
const db = await getDb();
const { category_type, category_name, description = null, display_order = 0 } = categoryData;
const [result] = await db.query(
`INSERT INTO issue_report_categories (category_type, category_name, description, display_order)
VALUES (?, ?, ?, ?)`,
[category_type, category_name, description, display_order]
);
callback(null, result.insertId);
} catch (err) {
callback(err);
}
};
/**
* 카테고리 수정
*/
const updateCategory = async (categoryId, categoryData, callback) => {
try {
const db = await getDb();
const { category_name, description, display_order, is_active } = categoryData;
const [result] = await db.query(
`UPDATE issue_report_categories
SET category_name = ?, description = ?, display_order = ?, is_active = ?
WHERE category_id = ?`,
[category_name, description, display_order, is_active, categoryId]
);
callback(null, result);
} catch (err) {
callback(err);
}
};
/**
* 카테고리 삭제
*/
const deleteCategory = async (categoryId, callback) => {
try {
const db = await getDb();
const [result] = await db.query(
`DELETE FROM issue_report_categories WHERE category_id = ?`,
[categoryId]
);
callback(null, result);
} catch (err) {
callback(err);
}
};
// ==================== 사전 정의 신고 항목 관리 ====================
/**
* 카테고리별 활성 항목 조회
*/
const getItemsByCategory = async (categoryId, callback) => {
try {
const db = await getDb();
const [rows] = await db.query(
`SELECT item_id, category_id, item_name, description, severity, display_order
FROM issue_report_items
WHERE category_id = ? AND is_active = TRUE
ORDER BY display_order, item_id`,
[categoryId]
);
callback(null, rows);
} catch (err) {
callback(err);
}
};
/**
* 모든 항목 조회 (관리용)
*/
const getAllItems = async (callback) => {
try {
const db = await getDb();
const [rows] = await db.query(
`SELECT iri.item_id, iri.category_id, iri.item_name, iri.description,
iri.severity, iri.display_order, iri.is_active, iri.created_at,
irc.category_name, irc.category_type
FROM issue_report_items iri
INNER JOIN issue_report_categories irc ON iri.category_id = irc.category_id
ORDER BY irc.category_type, irc.display_order, iri.display_order, iri.item_id`
);
callback(null, rows);
} catch (err) {
callback(err);
}
};
/**
* 항목 생성
*/
const createItem = async (itemData, callback) => {
try {
const db = await getDb();
const { category_id, item_name, description = null, severity = 'medium', display_order = 0 } = itemData;
const [result] = await db.query(
`INSERT INTO issue_report_items (category_id, item_name, description, severity, display_order)
VALUES (?, ?, ?, ?, ?)`,
[category_id, item_name, description, severity, display_order]
);
callback(null, result.insertId);
} catch (err) {
callback(err);
}
};
/**
* 항목 수정
*/
const updateItem = async (itemId, itemData, callback) => {
try {
const db = await getDb();
const { item_name, description, severity, display_order, is_active } = itemData;
const [result] = await db.query(
`UPDATE issue_report_items
SET item_name = ?, description = ?, severity = ?, display_order = ?, is_active = ?
WHERE item_id = ?`,
[item_name, description, severity, display_order, is_active, itemId]
);
callback(null, result);
} catch (err) {
callback(err);
}
};
/**
* 항목 삭제
*/
const deleteItem = async (itemId, callback) => {
try {
const db = await getDb();
const [result] = await db.query(
`DELETE FROM issue_report_items WHERE item_id = ?`,
[itemId]
);
callback(null, result);
} catch (err) {
callback(err);
}
};
/**
* 카테고리 ID로 단건 조회
*/
const getCategoryById = async (categoryId, callback) => {
try {
const db = await getDb();
const [rows] = await db.query(
`SELECT category_id, category_type, category_name, description
FROM issue_report_categories
WHERE category_id = ?`,
[categoryId]
);
callback(null, rows[0] || null);
} catch (err) {
callback(err);
}
};
// ==================== 문제 신고 관리 ====================
// 한국 시간 유틸리티 import
const { getKoreaDatetime } = require('../utils/dateUtils');
/**
* 신고 생성
*/
const createReport = async (reportData, callback) => {
try {
const db = await getDb();
const {
reporter_id,
factory_category_id = null,
workplace_id = null,
project_id = null,
custom_location = null,
tbm_session_id = null,
visit_request_id = null,
issue_category_id,
issue_item_id = null,
additional_description = null,
photo_path1 = null,
photo_path2 = null,
photo_path3 = null,
photo_path4 = null,
photo_path5 = null
} = reportData;
// 한국 시간 기준으로 신고 일시 설정
const reportDate = getKoreaDatetime();
const [result] = await db.query(
`INSERT INTO work_issue_reports
(reporter_id, report_date, factory_category_id, workplace_id, project_id, custom_location,
tbm_session_id, visit_request_id, issue_category_id, issue_item_id,
additional_description, photo_path1, photo_path2, photo_path3, photo_path4, photo_path5)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[reporter_id, reportDate, factory_category_id, workplace_id, project_id, custom_location,
tbm_session_id, visit_request_id, issue_category_id, issue_item_id,
additional_description, photo_path1, photo_path2, photo_path3, photo_path4, photo_path5]
);
// 상태 변경 로그 기록
await db.query(
`INSERT INTO work_issue_status_logs (report_id, previous_status, new_status, changed_by)
VALUES (?, NULL, 'reported', ?)`,
[result.insertId, reporter_id]
);
callback(null, result.insertId);
} catch (err) {
callback(err);
}
};
/**
* 신고 목록 조회 (필터 옵션 포함)
*/
const getAllReports = async (filters = {}, callback) => {
try {
const db = await getDb();
let query = `
SELECT
wir.report_id, wir.reporter_id, wir.report_date,
wir.factory_category_id, wir.workplace_id, wir.custom_location,
wir.tbm_session_id, wir.visit_request_id,
wir.issue_category_id, wir.issue_item_id, wir.additional_description,
wir.photo_path1, wir.photo_path2, wir.photo_path3, wir.photo_path4, wir.photo_path5,
wir.status, wir.assigned_department, wir.assigned_user_id, wir.assigned_at,
wir.resolution_notes, wir.resolved_at,
wir.created_at, wir.updated_at,
u.username as reporter_name, u.name as reporter_full_name,
wc.category_name as factory_name,
w.workplace_name,
irc.category_type, irc.category_name as issue_category_name,
iri.item_name as issue_item_name, iri.severity,
assignee.username as assigned_user_name, assignee.name as assigned_full_name
FROM work_issue_reports wir
INNER JOIN users u ON wir.reporter_id = u.user_id
LEFT JOIN workplace_categories wc ON wir.factory_category_id = wc.category_id
LEFT JOIN workplaces w ON wir.workplace_id = w.workplace_id
INNER JOIN issue_report_categories irc ON wir.issue_category_id = irc.category_id
LEFT JOIN issue_report_items iri ON wir.issue_item_id = iri.item_id
LEFT JOIN users assignee ON wir.assigned_user_id = assignee.user_id
WHERE 1=1
`;
const params = [];
// 필터 적용
if (filters.status) {
query += ` AND wir.status = ?`;
params.push(filters.status);
}
if (filters.category_type) {
query += ` AND irc.category_type = ?`;
params.push(filters.category_type);
}
if (filters.issue_category_id) {
query += ` AND wir.issue_category_id = ?`;
params.push(filters.issue_category_id);
}
if (filters.factory_category_id) {
query += ` AND wir.factory_category_id = ?`;
params.push(filters.factory_category_id);
}
if (filters.workplace_id) {
query += ` AND wir.workplace_id = ?`;
params.push(filters.workplace_id);
}
if (filters.reporter_id) {
query += ` AND wir.reporter_id = ?`;
params.push(filters.reporter_id);
}
if (filters.assigned_user_id) {
query += ` AND wir.assigned_user_id = ?`;
params.push(filters.assigned_user_id);
}
if (filters.start_date && filters.end_date) {
query += ` AND DATE(wir.report_date) BETWEEN ? AND ?`;
params.push(filters.start_date, filters.end_date);
}
if (filters.search) {
query += ` AND (wir.additional_description LIKE ? OR iri.item_name LIKE ? OR wir.custom_location LIKE ?)`;
const searchTerm = `%${filters.search}%`;
params.push(searchTerm, searchTerm, searchTerm);
}
query += ` ORDER BY wir.report_date DESC, wir.report_id DESC`;
// 페이지네이션
if (filters.limit) {
query += ` LIMIT ?`;
params.push(parseInt(filters.limit));
if (filters.offset) {
query += ` OFFSET ?`;
params.push(parseInt(filters.offset));
}
}
const [rows] = await db.query(query, params);
callback(null, rows);
} catch (err) {
callback(err);
}
};
/**
* 신고 상세 조회
*/
const getReportById = async (reportId, callback) => {
try {
const db = await getDb();
const [rows] = await db.query(
`SELECT
wir.report_id, wir.reporter_id, wir.report_date,
wir.factory_category_id, wir.workplace_id, wir.custom_location,
wir.tbm_session_id, wir.visit_request_id,
wir.issue_category_id, wir.issue_item_id, wir.additional_description,
wir.photo_path1, wir.photo_path2, wir.photo_path3, wir.photo_path4, wir.photo_path5,
wir.status, wir.assigned_department, wir.assigned_user_id, wir.assigned_at, wir.assigned_by,
wir.resolution_notes, wir.resolution_photo_path1, wir.resolution_photo_path2,
wir.resolved_at, wir.resolved_by,
wir.modification_history,
wir.created_at, wir.updated_at,
u.username as reporter_name, u.name as reporter_full_name,
wc.category_name as factory_name,
w.workplace_name,
irc.category_type, irc.category_name as issue_category_name,
iri.item_name as issue_item_name, iri.severity,
assignee.username as assigned_user_name, assignee.name as assigned_full_name,
assigner.username as assigned_by_name,
resolver.username as resolved_by_name
FROM work_issue_reports wir
INNER JOIN users u ON wir.reporter_id = u.user_id
LEFT JOIN workplace_categories wc ON wir.factory_category_id = wc.category_id
LEFT JOIN workplaces w ON wir.workplace_id = w.workplace_id
INNER JOIN issue_report_categories irc ON wir.issue_category_id = irc.category_id
LEFT JOIN issue_report_items iri ON wir.issue_item_id = iri.item_id
LEFT JOIN users assignee ON wir.assigned_user_id = assignee.user_id
LEFT JOIN users assigner ON wir.assigned_by = assigner.user_id
LEFT JOIN users resolver ON wir.resolved_by = resolver.user_id
WHERE wir.report_id = ?`,
[reportId]
);
callback(null, rows[0]);
} catch (err) {
callback(err);
}
};
/**
* 신고 수정
*/
const updateReport = async (reportId, reportData, userId, callback) => {
try {
const db = await getDb();
// 기존 데이터 조회
const [existing] = await db.query(
`SELECT * FROM work_issue_reports WHERE report_id = ?`,
[reportId]
);
if (existing.length === 0) {
return callback(new Error('신고를 찾을 수 없습니다.'));
}
const current = existing[0];
// 수정 이력 생성
const modifications = [];
const now = new Date().toISOString();
for (const key of Object.keys(reportData)) {
if (current[key] !== reportData[key] && reportData[key] !== undefined) {
modifications.push({
field: key,
old_value: current[key],
new_value: reportData[key],
modified_at: now,
modified_by: userId
});
}
}
// 기존 이력과 병합
const existingHistory = current.modification_history ? JSON.parse(current.modification_history) : [];
const newHistory = [...existingHistory, ...modifications];
const {
factory_category_id,
workplace_id,
custom_location,
issue_category_id,
issue_item_id,
additional_description,
photo_path1,
photo_path2,
photo_path3,
photo_path4,
photo_path5
} = reportData;
const [result] = await db.query(
`UPDATE work_issue_reports
SET factory_category_id = COALESCE(?, factory_category_id),
workplace_id = COALESCE(?, workplace_id),
custom_location = COALESCE(?, custom_location),
issue_category_id = COALESCE(?, issue_category_id),
issue_item_id = COALESCE(?, issue_item_id),
additional_description = COALESCE(?, additional_description),
photo_path1 = COALESCE(?, photo_path1),
photo_path2 = COALESCE(?, photo_path2),
photo_path3 = COALESCE(?, photo_path3),
photo_path4 = COALESCE(?, photo_path4),
photo_path5 = COALESCE(?, photo_path5),
modification_history = ?,
updated_at = NOW()
WHERE report_id = ?`,
[factory_category_id, workplace_id, custom_location,
issue_category_id, issue_item_id, additional_description,
photo_path1, photo_path2, photo_path3, photo_path4, photo_path5,
JSON.stringify(newHistory), reportId]
);
callback(null, result);
} catch (err) {
callback(err);
}
};
/**
* 신고 삭제
*/
const deleteReport = async (reportId, callback) => {
try {
const db = await getDb();
// 먼저 사진 경로 조회 (삭제용)
const [photos] = await db.query(
`SELECT photo_path1, photo_path2, photo_path3, photo_path4, photo_path5,
resolution_photo_path1, resolution_photo_path2
FROM work_issue_reports WHERE report_id = ?`,
[reportId]
);
const [result] = await db.query(
`DELETE FROM work_issue_reports WHERE report_id = ?`,
[reportId]
);
// 삭제할 사진 경로 반환
callback(null, { result, photos: photos[0] });
} catch (err) {
callback(err);
}
};
// ==================== 상태 관리 ====================
/**
* 신고 접수 (reported → received)
*/
const receiveReport = async (reportId, userId, callback) => {
try {
const db = await getDb();
// 현재 상태 확인
const [current] = await db.query(
`SELECT status FROM work_issue_reports WHERE report_id = ?`,
[reportId]
);
if (current.length === 0) {
return callback(new Error('신고를 찾을 수 없습니다.'));
}
if (current[0].status !== 'reported') {
return callback(new Error('접수 대기 상태가 아닙니다.'));
}
const [result] = await db.query(
`UPDATE work_issue_reports
SET status = 'received', updated_at = NOW()
WHERE report_id = ?`,
[reportId]
);
// 상태 변경 로그
await db.query(
`INSERT INTO work_issue_status_logs (report_id, previous_status, new_status, changed_by)
VALUES (?, 'reported', 'received', ?)`,
[reportId, userId]
);
callback(null, result);
} catch (err) {
callback(err);
}
};
/**
* 담당자 배정
*/
const assignReport = async (reportId, assignData, callback) => {
try {
const db = await getDb();
const { assigned_department, assigned_user_id, assigned_by } = assignData;
// 현재 상태 확인
const [current] = await db.query(
`SELECT status FROM work_issue_reports WHERE report_id = ?`,
[reportId]
);
if (current.length === 0) {
return callback(new Error('신고를 찾을 수 없습니다.'));
}
// 접수 상태 이상이어야 배정 가능
const validStatuses = ['received', 'in_progress'];
if (!validStatuses.includes(current[0].status)) {
return callback(new Error('접수된 상태에서만 담당자 배정이 가능합니다.'));
}
const [result] = await db.query(
`UPDATE work_issue_reports
SET assigned_department = ?, assigned_user_id = ?,
assigned_at = NOW(), assigned_by = ?, updated_at = NOW()
WHERE report_id = ?`,
[assigned_department, assigned_user_id, assigned_by, reportId]
);
callback(null, result);
} catch (err) {
callback(err);
}
};
/**
* 처리 시작 (received → in_progress)
*/
const startProcessing = async (reportId, userId, callback) => {
try {
const db = await getDb();
// 현재 상태 확인
const [current] = await db.query(
`SELECT status FROM work_issue_reports WHERE report_id = ?`,
[reportId]
);
if (current.length === 0) {
return callback(new Error('신고를 찾을 수 없습니다.'));
}
if (current[0].status !== 'received') {
return callback(new Error('접수된 상태에서만 처리를 시작할 수 있습니다.'));
}
const [result] = await db.query(
`UPDATE work_issue_reports
SET status = 'in_progress', updated_at = NOW()
WHERE report_id = ?`,
[reportId]
);
// 상태 변경 로그
await db.query(
`INSERT INTO work_issue_status_logs (report_id, previous_status, new_status, changed_by)
VALUES (?, 'received', 'in_progress', ?)`,
[reportId, userId]
);
callback(null, result);
} catch (err) {
callback(err);
}
};
/**
* 처리 완료 (in_progress → completed)
*/
const completeReport = async (reportId, completionData, callback) => {
try {
const db = await getDb();
const { resolution_notes, resolution_photo_path1, resolution_photo_path2, resolved_by } = completionData;
// 현재 상태 확인
const [current] = await db.query(
`SELECT status FROM work_issue_reports WHERE report_id = ?`,
[reportId]
);
if (current.length === 0) {
return callback(new Error('신고를 찾을 수 없습니다.'));
}
if (current[0].status !== 'in_progress') {
return callback(new Error('처리 중 상태에서만 완료할 수 있습니다.'));
}
const [result] = await db.query(
`UPDATE work_issue_reports
SET status = 'completed', resolution_notes = ?,
resolution_photo_path1 = ?, resolution_photo_path2 = ?,
resolved_at = NOW(), resolved_by = ?, updated_at = NOW()
WHERE report_id = ?`,
[resolution_notes, resolution_photo_path1, resolution_photo_path2, resolved_by, reportId]
);
// 상태 변경 로그
await db.query(
`INSERT INTO work_issue_status_logs (report_id, previous_status, new_status, changed_by, change_reason)
VALUES (?, 'in_progress', 'completed', ?, ?)`,
[reportId, resolved_by, resolution_notes]
);
callback(null, result);
} catch (err) {
callback(err);
}
};
/**
* 신고 종료 (completed → closed)
*/
const closeReport = async (reportId, userId, callback) => {
try {
const db = await getDb();
// 현재 상태 확인
const [current] = await db.query(
`SELECT status FROM work_issue_reports WHERE report_id = ?`,
[reportId]
);
if (current.length === 0) {
return callback(new Error('신고를 찾을 수 없습니다.'));
}
if (current[0].status !== 'completed') {
return callback(new Error('완료된 상태에서만 종료할 수 있습니다.'));
}
const [result] = await db.query(
`UPDATE work_issue_reports
SET status = 'closed', updated_at = NOW()
WHERE report_id = ?`,
[reportId]
);
// 상태 변경 로그
await db.query(
`INSERT INTO work_issue_status_logs (report_id, previous_status, new_status, changed_by)
VALUES (?, 'completed', 'closed', ?)`,
[reportId, userId]
);
callback(null, result);
} catch (err) {
callback(err);
}
};
/**
* 상태 변경 이력 조회
*/
const getStatusLogs = async (reportId, callback) => {
try {
const db = await getDb();
const [rows] = await db.query(
`SELECT wisl.log_id, wisl.report_id, wisl.previous_status, wisl.new_status,
wisl.changed_by, wisl.change_reason, wisl.changed_at,
u.username as changed_by_name, u.name as changed_by_full_name
FROM work_issue_status_logs wisl
INNER JOIN users u ON wisl.changed_by = u.user_id
WHERE wisl.report_id = ?
ORDER BY wisl.changed_at ASC`,
[reportId]
);
callback(null, rows);
} catch (err) {
callback(err);
}
};
/**
* m_project_id 업데이트 (System 3 연동 후)
*/
const updateMProjectId = async (reportId, mProjectId, callback) => {
try {
const db = await getDb();
await db.query(
`UPDATE work_issue_reports SET m_project_id = ? WHERE report_id = ?`,
[mProjectId, reportId]
);
callback(null);
} catch (err) {
callback(err);
}
};
// ==================== 통계 ====================
/**
* 신고 통계 요약
*/
const getStatsSummary = async (filters = {}, callback) => {
try {
const db = await getDb();
let whereClause = '1=1';
const params = [];
let joinClause = '';
if (filters.category_type) {
joinClause = ' INNER JOIN issue_report_categories irc ON wir.issue_category_id = irc.category_id';
whereClause += ` AND irc.category_type = ?`;
params.push(filters.category_type);
}
if (filters.start_date && filters.end_date) {
whereClause += ` AND DATE(wir.report_date) BETWEEN ? AND ?`;
params.push(filters.start_date, filters.end_date);
}
if (filters.factory_category_id) {
whereClause += ` AND wir.factory_category_id = ?`;
params.push(filters.factory_category_id);
}
const [rows] = await db.query(
`SELECT
COUNT(*) as total,
SUM(CASE WHEN wir.status = 'reported' THEN 1 ELSE 0 END) as reported,
SUM(CASE WHEN wir.status = 'received' THEN 1 ELSE 0 END) as received,
SUM(CASE WHEN wir.status = 'in_progress' THEN 1 ELSE 0 END) as in_progress,
SUM(CASE WHEN wir.status = 'completed' THEN 1 ELSE 0 END) as completed,
SUM(CASE WHEN wir.status = 'closed' THEN 1 ELSE 0 END) as closed
FROM work_issue_reports wir${joinClause}
WHERE ${whereClause}`,
params
);
callback(null, rows[0]);
} catch (err) {
callback(err);
}
};
/**
* 카테고리별 통계
*/
const getStatsByCategory = async (filters = {}, callback) => {
try {
const db = await getDb();
let whereClause = '1=1';
const params = [];
if (filters.start_date && filters.end_date) {
whereClause += ` AND DATE(wir.report_date) BETWEEN ? AND ?`;
params.push(filters.start_date, filters.end_date);
}
const [rows] = await db.query(
`SELECT
irc.category_type, irc.category_name,
COUNT(*) as count
FROM work_issue_reports wir
INNER JOIN issue_report_categories irc ON wir.issue_category_id = irc.category_id
WHERE ${whereClause}
GROUP BY irc.category_id
ORDER BY irc.category_type, count DESC`,
params
);
callback(null, rows);
} catch (err) {
callback(err);
}
};
/**
* 작업장별 통계
*/
const getStatsByWorkplace = async (filters = {}, callback) => {
try {
const db = await getDb();
let whereClause = 'wir.workplace_id IS NOT NULL';
const params = [];
if (filters.start_date && filters.end_date) {
whereClause += ` AND DATE(wir.report_date) BETWEEN ? AND ?`;
params.push(filters.start_date, filters.end_date);
}
if (filters.factory_category_id) {
whereClause += ` AND wir.factory_category_id = ?`;
params.push(filters.factory_category_id);
}
const [rows] = await db.query(
`SELECT
wir.factory_category_id, wc.category_name as factory_name,
wir.workplace_id, w.workplace_name,
COUNT(*) as count
FROM work_issue_reports wir
INNER JOIN workplace_categories wc ON wir.factory_category_id = wc.category_id
INNER JOIN workplaces w ON wir.workplace_id = w.workplace_id
WHERE ${whereClause}
GROUP BY wir.factory_category_id, wir.workplace_id
ORDER BY count DESC`,
params
);
callback(null, rows);
} catch (err) {
callback(err);
}
};
module.exports = {
// 카테고리
getAllCategories,
getCategoriesByType,
getCategoryById,
createCategory,
updateCategory,
deleteCategory,
// 항목
getItemsByCategory,
getAllItems,
createItem,
updateItem,
deleteItem,
// 신고
createReport,
getAllReports,
getReportById,
updateReport,
deleteReport,
// System 3 연동
updateMProjectId,
// 상태 관리
receiveReport,
assignReport,
startProcessing,
completeReport,
closeReport,
getStatusLogs,
// 통계
getStatsSummary,
getStatsByCategory,
getStatsByWorkplace
};

View File

@@ -18,8 +18,20 @@ const PORT = process.env.PORT || 3005;
app.set('trust proxy', 1);
// CORS
const allowedOrigins = [
'https://tkfb.technicalkorea.net',
'https://tkreport.technicalkorea.net',
'https://tkqc.technicalkorea.net',
'https://tkuser.technicalkorea.net',
];
if (process.env.NODE_ENV === 'development') {
allowedOrigins.push('http://localhost:30080', 'http://localhost:30180', 'http://localhost:30280');
}
app.use(cors({
origin: true,
origin: function(origin, cb) {
if (!origin || allowedOrigins.includes(origin) || /^http:\/\/192\.168\.\d+\.\d+(:\d+)?$/.test(origin)) return cb(null, true);
cb(new Error('CORS blocked: ' + origin));
},
credentials: true
}));

View File

@@ -10,25 +10,20 @@ const { getDb } = require('../config/database');
/**
* 모든 신고 카테고리 조회
*/
const getAllCategories = async (callback) => {
try {
const getAllCategories = async () => {
const db = await getDb();
const [rows] = await db.query(
`SELECT category_id, category_type, category_name, description, display_order, is_active, created_at
FROM issue_report_categories
ORDER BY category_type, display_order, category_id`
);
callback(null, rows);
} catch (err) {
callback(err);
}
return rows;
};
/**
* 타입별 활성 카테고리 조회 (nonconformity/safety)
*/
const getCategoriesByType = async (categoryType, callback) => {
try {
const getCategoriesByType = async (categoryType) => {
const db = await getDb();
const [rows] = await db.query(
`SELECT category_id, category_type, category_name, description, display_order
@@ -37,17 +32,13 @@ const getCategoriesByType = async (categoryType, callback) => {
ORDER BY display_order, category_id`,
[categoryType]
);
callback(null, rows);
} catch (err) {
callback(err);
}
return rows;
};
/**
* 카테고리 생성
*/
const createCategory = async (categoryData, callback) => {
try {
const createCategory = async (categoryData) => {
const db = await getDb();
const { category_type, category_name, description = null, display_order = 0 } = categoryData;
@@ -57,17 +48,13 @@ const createCategory = async (categoryData, callback) => {
[category_type, category_name, description, display_order]
);
callback(null, result.insertId);
} catch (err) {
callback(err);
}
return result.insertId;
};
/**
* 카테고리 수정
*/
const updateCategory = async (categoryId, categoryData, callback) => {
try {
const updateCategory = async (categoryId, categoryData) => {
const db = await getDb();
const { category_name, description, display_order, is_active } = categoryData;
@@ -78,26 +65,19 @@ const updateCategory = async (categoryId, categoryData, callback) => {
[category_name, description, display_order, is_active, categoryId]
);
callback(null, result);
} catch (err) {
callback(err);
}
return result;
};
/**
* 카테고리 삭제
*/
const deleteCategory = async (categoryId, callback) => {
try {
const deleteCategory = async (categoryId) => {
const db = await getDb();
const [result] = await db.query(
`DELETE FROM issue_report_categories WHERE category_id = ?`,
[categoryId]
);
callback(null, result);
} catch (err) {
callback(err);
}
return result;
};
// ==================== 사전 정의 신고 항목 관리 ====================
@@ -105,8 +85,7 @@ const deleteCategory = async (categoryId, callback) => {
/**
* 카테고리별 활성 항목 조회
*/
const getItemsByCategory = async (categoryId, callback) => {
try {
const getItemsByCategory = async (categoryId) => {
const db = await getDb();
const [rows] = await db.query(
`SELECT item_id, category_id, item_name, description, severity, display_order
@@ -115,17 +94,13 @@ const getItemsByCategory = async (categoryId, callback) => {
ORDER BY display_order, item_id`,
[categoryId]
);
callback(null, rows);
} catch (err) {
callback(err);
}
return rows;
};
/**
* 모든 항목 조회 (관리용)
*/
const getAllItems = async (callback) => {
try {
const getAllItems = async () => {
const db = await getDb();
const [rows] = await db.query(
`SELECT iri.item_id, iri.category_id, iri.item_name, iri.description,
@@ -135,17 +110,13 @@ const getAllItems = async (callback) => {
INNER JOIN issue_report_categories irc ON iri.category_id = irc.category_id
ORDER BY irc.category_type, irc.display_order, iri.display_order, iri.item_id`
);
callback(null, rows);
} catch (err) {
callback(err);
}
return rows;
};
/**
* 항목 생성
*/
const createItem = async (itemData, callback) => {
try {
const createItem = async (itemData) => {
const db = await getDb();
const { category_id, item_name, description = null, severity = 'medium', display_order = 0 } = itemData;
@@ -155,17 +126,13 @@ const createItem = async (itemData, callback) => {
[category_id, item_name, description, severity, display_order]
);
callback(null, result.insertId);
} catch (err) {
callback(err);
}
return result.insertId;
};
/**
* 항목 수정
*/
const updateItem = async (itemId, itemData, callback) => {
try {
const updateItem = async (itemId, itemData) => {
const db = await getDb();
const { item_name, description, severity, display_order, is_active } = itemData;
@@ -176,33 +143,25 @@ const updateItem = async (itemId, itemData, callback) => {
[item_name, description, severity, display_order, is_active, itemId]
);
callback(null, result);
} catch (err) {
callback(err);
}
return result;
};
/**
* 항목 삭제
*/
const deleteItem = async (itemId, callback) => {
try {
const deleteItem = async (itemId) => {
const db = await getDb();
const [result] = await db.query(
`DELETE FROM issue_report_items WHERE item_id = ?`,
[itemId]
);
callback(null, result);
} catch (err) {
callback(err);
}
return result;
};
/**
* 카테고리 ID로 단건 조회
*/
const getCategoryById = async (categoryId, callback) => {
try {
const getCategoryById = async (categoryId) => {
const db = await getDb();
const [rows] = await db.query(
`SELECT category_id, category_type, category_name, description
@@ -210,10 +169,7 @@ const getCategoryById = async (categoryId, callback) => {
WHERE category_id = ?`,
[categoryId]
);
callback(null, rows[0] || null);
} catch (err) {
callback(err);
}
return rows[0] || null;
};
// ==================== 문제 신고 관리 ====================
@@ -224,8 +180,7 @@ const { getKoreaDatetime } = require('../utils/dateUtils');
/**
* 신고 생성
*/
const createReport = async (reportData, callback) => {
try {
const createReport = async (reportData) => {
const db = await getDb();
const {
reporter_id,
@@ -267,17 +222,13 @@ const createReport = async (reportData, callback) => {
[result.insertId, reporter_id]
);
callback(null, result.insertId);
} catch (err) {
callback(err);
}
return result.insertId;
};
/**
* 신고 목록 조회 (필터 옵션 포함)
*/
const getAllReports = async (filters = {}, callback) => {
try {
const getAllReports = async (filters = {}) => {
const db = await getDb();
let query = `
SELECT
@@ -368,17 +319,13 @@ const getAllReports = async (filters = {}, callback) => {
}
const [rows] = await db.query(query, params);
callback(null, rows);
} catch (err) {
callback(err);
}
return rows;
};
/**
* 신고 상세 조회
*/
const getReportById = async (reportId, callback) => {
try {
const getReportById = async (reportId) => {
const db = await getDb();
const [rows] = await db.query(
`SELECT
@@ -413,17 +360,13 @@ const getReportById = async (reportId, callback) => {
[reportId]
);
callback(null, rows[0]);
} catch (err) {
callback(err);
}
return rows[0];
};
/**
* 신고 수정
*/
const updateReport = async (reportId, reportData, userId, callback) => {
try {
const updateReport = async (reportId, reportData, userId) => {
const db = await getDb();
// 기존 데이터 조회
@@ -433,7 +376,7 @@ const updateReport = async (reportId, reportData, userId, callback) => {
);
if (existing.length === 0) {
return callback(new Error('신고를 찾을 수 없습니다.'));
throw new Error('신고를 찾을 수 없습니다.');
}
const current = existing[0];
@@ -494,17 +437,13 @@ const updateReport = async (reportId, reportData, userId, callback) => {
JSON.stringify(newHistory), reportId]
);
callback(null, result);
} catch (err) {
callback(err);
}
return result;
};
/**
* 신고 삭제
*/
const deleteReport = async (reportId, callback) => {
try {
const deleteReport = async (reportId) => {
const db = await getDb();
// 먼저 사진 경로 조회 (삭제용)
@@ -521,10 +460,7 @@ const deleteReport = async (reportId, callback) => {
);
// 삭제할 사진 경로 반환
callback(null, { result, photos: photos[0] });
} catch (err) {
callback(err);
}
return { result, photos: photos[0] };
};
// ==================== 상태 관리 ====================
@@ -532,8 +468,7 @@ const deleteReport = async (reportId, callback) => {
/**
* 신고 접수 (reported → received)
*/
const receiveReport = async (reportId, userId, callback) => {
try {
const receiveReport = async (reportId, userId) => {
const db = await getDb();
// 현재 상태 확인
@@ -543,11 +478,11 @@ const receiveReport = async (reportId, userId, callback) => {
);
if (current.length === 0) {
return callback(new Error('신고를 찾을 수 없습니다.'));
throw new Error('신고를 찾을 수 없습니다.');
}
if (current[0].status !== 'reported') {
return callback(new Error('접수 대기 상태가 아닙니다.'));
throw new Error('접수 대기 상태가 아닙니다.');
}
const [result] = await db.query(
@@ -564,17 +499,13 @@ const receiveReport = async (reportId, userId, callback) => {
[reportId, userId]
);
callback(null, result);
} catch (err) {
callback(err);
}
return result;
};
/**
* 담당자 배정
*/
const assignReport = async (reportId, assignData, callback) => {
try {
const assignReport = async (reportId, assignData) => {
const db = await getDb();
const { assigned_department, assigned_user_id, assigned_by } = assignData;
@@ -585,13 +516,13 @@ const assignReport = async (reportId, assignData, callback) => {
);
if (current.length === 0) {
return callback(new Error('신고를 찾을 수 없습니다.'));
throw new Error('신고를 찾을 수 없습니다.');
}
// 접수 상태 이상이어야 배정 가능
const validStatuses = ['received', 'in_progress'];
if (!validStatuses.includes(current[0].status)) {
return callback(new Error('접수된 상태에서만 담당자 배정이 가능합니다.'));
throw new Error('접수된 상태에서만 담당자 배정이 가능합니다.');
}
const [result] = await db.query(
@@ -602,17 +533,13 @@ const assignReport = async (reportId, assignData, callback) => {
[assigned_department, assigned_user_id, assigned_by, reportId]
);
callback(null, result);
} catch (err) {
callback(err);
}
return result;
};
/**
* 처리 시작 (received → in_progress)
*/
const startProcessing = async (reportId, userId, callback) => {
try {
const startProcessing = async (reportId, userId) => {
const db = await getDb();
// 현재 상태 확인
@@ -622,11 +549,11 @@ const startProcessing = async (reportId, userId, callback) => {
);
if (current.length === 0) {
return callback(new Error('신고를 찾을 수 없습니다.'));
throw new Error('신고를 찾을 수 없습니다.');
}
if (current[0].status !== 'received') {
return callback(new Error('접수된 상태에서만 처리를 시작할 수 있습니다.'));
throw new Error('접수된 상태에서만 처리를 시작할 수 있습니다.');
}
const [result] = await db.query(
@@ -643,17 +570,13 @@ const startProcessing = async (reportId, userId, callback) => {
[reportId, userId]
);
callback(null, result);
} catch (err) {
callback(err);
}
return result;
};
/**
* 처리 완료 (in_progress → completed)
*/
const completeReport = async (reportId, completionData, callback) => {
try {
const completeReport = async (reportId, completionData) => {
const db = await getDb();
const { resolution_notes, resolution_photo_path1, resolution_photo_path2, resolved_by } = completionData;
@@ -664,11 +587,11 @@ const completeReport = async (reportId, completionData, callback) => {
);
if (current.length === 0) {
return callback(new Error('신고를 찾을 수 없습니다.'));
throw new Error('신고를 찾을 수 없습니다.');
}
if (current[0].status !== 'in_progress') {
return callback(new Error('처리 중 상태에서만 완료할 수 있습니다.'));
throw new Error('처리 중 상태에서만 완료할 수 있습니다.');
}
const [result] = await db.query(
@@ -687,17 +610,13 @@ const completeReport = async (reportId, completionData, callback) => {
[reportId, resolved_by, resolution_notes]
);
callback(null, result);
} catch (err) {
callback(err);
}
return result;
};
/**
* 신고 종료 (completed → closed)
*/
const closeReport = async (reportId, userId, callback) => {
try {
const closeReport = async (reportId, userId) => {
const db = await getDb();
// 현재 상태 확인
@@ -707,11 +626,11 @@ const closeReport = async (reportId, userId, callback) => {
);
if (current.length === 0) {
return callback(new Error('신고를 찾을 수 없습니다.'));
throw new Error('신고를 찾을 수 없습니다.');
}
if (current[0].status !== 'completed') {
return callback(new Error('완료된 상태에서만 종료할 수 있습니다.'));
throw new Error('완료된 상태에서만 종료할 수 있습니다.');
}
const [result] = await db.query(
@@ -728,17 +647,13 @@ const closeReport = async (reportId, userId, callback) => {
[reportId, userId]
);
callback(null, result);
} catch (err) {
callback(err);
}
return result;
};
/**
* 상태 변경 이력 조회
*/
const getStatusLogs = async (reportId, callback) => {
try {
const getStatusLogs = async (reportId) => {
const db = await getDb();
const [rows] = await db.query(
`SELECT wisl.log_id, wisl.report_id, wisl.previous_status, wisl.new_status,
@@ -750,26 +665,19 @@ const getStatusLogs = async (reportId, callback) => {
ORDER BY wisl.changed_at ASC`,
[reportId]
);
callback(null, rows);
} catch (err) {
callback(err);
}
return rows;
};
/**
* m_project_id 업데이트 (System 3 연동 후)
*/
const updateMProjectId = async (reportId, mProjectId, callback) => {
try {
const updateMProjectId = async (reportId, mProjectId) => {
const db = await getDb();
await db.query(
`UPDATE work_issue_reports SET m_project_id = ? WHERE report_id = ?`,
[mProjectId, reportId]
);
callback(null);
} catch (err) {
callback(err);
}
return;
};
// ==================== 통계 ====================
@@ -777,8 +685,7 @@ const updateMProjectId = async (reportId, mProjectId, callback) => {
/**
* 신고 통계 요약
*/
const getStatsSummary = async (filters = {}, callback) => {
try {
const getStatsSummary = async (filters = {}) => {
const db = await getDb();
let whereClause = '1=1';
@@ -812,17 +719,13 @@ const getStatsSummary = async (filters = {}, callback) => {
params
);
callback(null, rows[0]);
} catch (err) {
callback(err);
}
return rows[0];
};
/**
* 카테고리별 통계
*/
const getStatsByCategory = async (filters = {}, callback) => {
try {
const getStatsByCategory = async (filters = {}) => {
const db = await getDb();
let whereClause = '1=1';
@@ -845,17 +748,13 @@ const getStatsByCategory = async (filters = {}, callback) => {
params
);
callback(null, rows);
} catch (err) {
callback(err);
}
return rows;
};
/**
* 작업장별 통계
*/
const getStatsByWorkplace = async (filters = {}, callback) => {
try {
const getStatsByWorkplace = async (filters = {}) => {
const db = await getDb();
let whereClause = 'wir.workplace_id IS NOT NULL';
@@ -885,17 +784,13 @@ const getStatsByWorkplace = async (filters = {}, callback) => {
params
);
callback(null, rows);
} catch (err) {
callback(err);
}
return rows;
};
/**
* 유형 이관 (category_type 변경)
*/
const transferCategoryType = async (reportId, newCategoryType, userId, callback) => {
try {
const transferCategoryType = async (reportId, newCategoryType, userId) => {
const db = await getDb();
// 기존 데이터 조회
@@ -905,14 +800,14 @@ const transferCategoryType = async (reportId, newCategoryType, userId, callback)
);
if (existing.length === 0) {
return callback(new Error('신고를 찾을 수 없습니다.'));
throw new Error('신고를 찾을 수 없습니다.');
}
const current = existing[0];
const oldCategoryType = current.category_type;
if (oldCategoryType === newCategoryType) {
return callback(new Error('현재 유형과 동일합니다.'));
throw new Error('현재 유형과 동일합니다.');
}
// 수정 이력 추가
@@ -934,17 +829,13 @@ const transferCategoryType = async (reportId, newCategoryType, userId, callback)
[newCategoryType, JSON.stringify(existingHistory), reportId]
);
callback(null, result);
} catch (err) {
callback(err);
}
return result;
};
/**
* 공장/작업장 이름 조회 (System 3 연동용)
*/
const getLocationNames = async (factoryCategoryId, workplaceId, callback) => {
try {
const getLocationNames = async (factoryCategoryId, workplaceId) => {
const db = await getDb();
let factoryName = null;
let workplaceName = null;
@@ -965,10 +856,7 @@ const getLocationNames = async (factoryCategoryId, workplaceId, callback) => {
if (rows.length > 0) workplaceName = rows[0].workplace_name;
}
callback(null, { factory_name: factoryName, workplace_name: workplaceName });
} catch (err) {
callback(err);
}
return { factory_name: factoryName, workplace_name: workplaceName };
};
module.exports = {

View File

@@ -45,9 +45,9 @@
cookieRemove('sso_token');
cookieRemove('sso_user');
cookieRemove('sso_refresh_token');
localStorage.removeItem('sso_token');
localStorage.removeItem('sso_user');
localStorage.removeItem('sso_refresh_token');
['sso_token','sso_user','sso_refresh_token','token','user','access_token','currentUser','current_user','userInfo','userPageAccess'].forEach(function(k) {
localStorage.removeItem(k);
});
};
// ==================== 보안 유틸리티 (XSS 방지) ====================

View File

@@ -1,3 +1,4 @@
import os
from contextlib import asynccontextmanager
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
@@ -32,11 +33,19 @@ app = FastAPI(
lifespan=lifespan
)
# CORS 설정 (완전 개방 - CORS 문제 해결)
ALLOWED_ORIGINS = [
"https://tkfb.technicalkorea.net",
"https://tkreport.technicalkorea.net",
"https://tkqc.technicalkorea.net",
"https://tkuser.technicalkorea.net",
]
if os.getenv("ENV", "production") == "development":
ALLOWED_ORIGINS += ["http://localhost:30080", "http://localhost:30180", "http://localhost:30280"]
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=False, # * origin과 credentials는 함께 사용 불가
allow_origins=ALLOWED_ORIGINS,
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
allow_headers=["*"],
expose_headers=["*"]

View File

@@ -53,32 +53,28 @@ const API_BASE_URL = (() => {
// 토큰 관리 (SSO 쿠키 + localStorage 이중 지원)
const TokenManager = {
getToken: () => {
// SSO 쿠키 우선 (sso_token), localStorage 폴백 (access_token)
return _cookieGet('sso_token') || localStorage.getItem('sso_token') || localStorage.getItem('access_token');
// SSO 쿠키 우선, localStorage 폴백
return _cookieGet('sso_token') || localStorage.getItem('sso_token');
},
setToken: (token) => localStorage.setItem('access_token', token),
setToken: (token) => localStorage.setItem('sso_token', token),
removeToken: () => {
_cookieRemove('sso_token');
_cookieRemove('sso_user');
_cookieRemove('sso_refresh_token');
localStorage.removeItem('access_token');
localStorage.removeItem('sso_token');
localStorage.removeItem('sso_user');
},
getUser: () => {
// SSO 쿠키 우선, localStorage 폴백
const ssoUser = _cookieGet('sso_user') || localStorage.getItem('sso_user');
if (ssoUser) {
try { return JSON.parse(ssoUser); } catch(e) {}
}
const userStr = localStorage.getItem('currentUser') || localStorage.getItem('current_user');
return userStr ? JSON.parse(userStr) : null;
return null;
},
setUser: (user) => localStorage.setItem('current_user', JSON.stringify(user)),
setUser: (user) => localStorage.setItem('sso_user', JSON.stringify(user)),
removeUser: () => {
localStorage.removeItem('current_user');
localStorage.removeItem('currentUser');
localStorage.removeItem('sso_user');
}
};

View File

@@ -47,23 +47,17 @@ class App {
*/
async checkAuth() {
// SSO 쿠키 우선, localStorage 폴백
const token = this._cookieGet('sso_token') || localStorage.getItem('sso_token') || localStorage.getItem('access_token');
const token = this._cookieGet('sso_token') || localStorage.getItem('sso_token');
if (!token) {
throw new Error('토큰 없음');
}
// SSO 쿠키에서 사용자 정보 시도
const ssoUser = this._cookieGet('sso_user') || localStorage.getItem('sso_user');
if (ssoUser) {
try { this.currentUser = JSON.parse(ssoUser); return; } catch(e) {}
}
const storedUser = localStorage.getItem('currentUser');
if (storedUser) {
this.currentUser = JSON.parse(storedUser);
} else {
throw new Error('사용자 정보 없음');
}
}
_cookieGet(name) {
const match = document.cookie.match(new RegExp('(?:^|; )' + name + '=([^;]*)'));
@@ -371,10 +365,8 @@ class App {
if (window.authManager) {
window.authManager.clearAuth();
} else {
localStorage.removeItem('access_token');
localStorage.removeItem('sso_token');
localStorage.removeItem('sso_user');
localStorage.removeItem('currentUser');
}
this.redirectToLogin();
}

View File

@@ -628,10 +628,10 @@ class CommonHeader {
if (window.authManager) {
window.authManager.logout();
} else {
localStorage.removeItem('access_token');
localStorage.removeItem('sso_token');
localStorage.removeItem('sso_token');
localStorage.removeItem('sso_user');
localStorage.removeItem('currentUser');
localStorage.removeItem('sso_user');
var hostname = window.location.hostname;
if (hostname.includes('technicalkorea.net')) {
window.location.href = window.location.protocol + '//tkfb.technicalkorea.net/login';
@@ -667,7 +667,6 @@ class CommonHeader {
initializeKeyboardShortcuts() {
if (window.keyboardShortcuts) {
window.keyboardShortcuts.setUser(this.currentUser);
console.log('⌨️ 키보드 단축키 사용자 설정 완료');
}
}
@@ -679,7 +678,6 @@ class CommonHeader {
// 사용자 설정 후 프리로더 초기화
setTimeout(() => {
window.pagePreloader.init();
console.log('🚀 페이지 프리로더 초기화 완료');
}, 1000); // 권한 시스템 로드 후 실행
}
}

View File

@@ -18,7 +18,6 @@ class AuthManager {
* 초기화
*/
init() {
console.log('🔐 AuthManager 초기화');
// localStorage에서 사용자 정보 복원
this.restoreUserFromStorage();
@@ -57,7 +56,7 @@ class AuthManager {
* SSO 토큰 가져오기 (쿠키 우선, localStorage 폴백)
*/
_getToken() {
return this._cookieGet('sso_token') || localStorage.getItem('sso_token') || localStorage.getItem('access_token');
return this._cookieGet('sso_token') || localStorage.getItem('sso_token');
}
/**
@@ -68,10 +67,6 @@ class AuthManager {
if (ssoUser && ssoUser !== 'undefined' && ssoUser !== 'null') {
try { return JSON.parse(ssoUser); } catch(e) {}
}
const userStr = localStorage.getItem('currentUser');
if (userStr && userStr !== 'undefined' && userStr !== 'null') {
try { return JSON.parse(userStr); } catch(e) {}
}
return null;
}
@@ -148,7 +143,7 @@ class AuthManager {
this.lastAuthCheck = Date.now();
// localStorage 업데이트
localStorage.setItem('currentUser', JSON.stringify(user));
localStorage.setItem('sso_user', JSON.stringify(user));
this.notifyListeners('auth-success', user);
return user;
@@ -191,11 +186,10 @@ class AuthManager {
this._cookieRemove('sso_user');
this._cookieRemove('sso_refresh_token');
// localStorage 삭제
localStorage.removeItem('access_token');
localStorage.removeItem('sso_token');
localStorage.removeItem('sso_user');
localStorage.removeItem('currentUser');
// localStorage 삭제 (전 시스템 키 통일)
['sso_token','sso_user','sso_refresh_token','token','user','access_token','currentUser','current_user','userInfo','userPageAccess'].forEach(k => {
localStorage.removeItem(k);
});
this.notifyListeners('auth-cleared');
}
@@ -212,9 +206,9 @@ class AuthManager {
this.isAuthenticated = true;
this.lastAuthCheck = Date.now();
// localStorage 저장
localStorage.setItem('access_token', data.access_token);
localStorage.setItem('currentUser', JSON.stringify(data.user));
// localStorage 저장 (sso_token/sso_user로 통일)
localStorage.setItem('sso_token', data.access_token);
localStorage.setItem('sso_user', JSON.stringify(data.user));
this.notifyListeners('login-success', data.user);
return data;
@@ -293,4 +287,3 @@ class AuthManager {
// 전역 인스턴스 생성
window.authManager = new AuthManager();
console.log('🎯 AuthManager 로드 완료');

View File

@@ -36,7 +36,6 @@ class KeyboardShortcutManager {
this.register('r', () => this.triggerRefreshAction(), '새로고침');
this.register('f', () => this.focusSearchField(), '검색 포커스');
console.log('⌨️ 키보드 단축키 등록 완료');
}
/**
@@ -178,7 +177,6 @@ class KeyboardShortcutManager {
// 콜백 실행
try {
shortcut.callback(event);
console.log(`⌨️ 단축키 실행: ${combination}`);
} catch (error) {
console.error('단축키 실행 실패:', combination, error);
}
@@ -599,7 +597,6 @@ class KeyboardShortcutManager {
*/
setEnabled(enabled) {
this.isEnabled = enabled;
console.log(`⌨️ 키보드 단축키 ${enabled ? '활성화' : '비활성화'}`);
}
/**

View File

@@ -52,7 +52,7 @@ class PageManager {
* 사용자 인증 확인
*/
async checkAuthentication() {
const token = localStorage.getItem('access_token');
const token = localStorage.getItem('sso_token');
if (!token) {
window.location.href = '/index.html';
return null;
@@ -63,12 +63,12 @@ class PageManager {
await this.waitForAPI();
const user = await AuthAPI.getCurrentUser();
localStorage.setItem('currentUser', JSON.stringify(user));
localStorage.setItem('sso_user', JSON.stringify(user));
return user;
} catch (error) {
console.error('인증 실패:', error);
localStorage.removeItem('access_token');
localStorage.removeItem('currentUser');
localStorage.removeItem('sso_token');
localStorage.removeItem('sso_user');
window.location.href = '/index.html';
return null;
}

View File

@@ -83,7 +83,6 @@ class PagePreloader {
if (this.isPreloading) return;
this.isPreloading = true;
console.log('🚀 페이지 프리로딩 시작:', pages.map(p => p.id));
for (const page of pages) {
if (this.preloadedPages.has(page.url)) continue;
@@ -93,7 +92,6 @@ class PagePreloader {
// 네트워크 상태 확인 (느린 연결에서는 중단)
if (this.isSlowConnection()) {
console.log('⚠️ 느린 연결 감지, 프리로딩 중단');
break;
}
@@ -106,7 +104,6 @@ class PagePreloader {
}
this.isPreloading = false;
console.log('✅ 페이지 프리로딩 완료');
}
/**
@@ -128,7 +125,6 @@ class PagePreloader {
await this.preloadPageResources(html, page.url);
this.preloadedPages.add(page.url);
console.log(`📄 프리로드 완료: ${page.id}`);
}
} catch (error) {
@@ -251,7 +247,6 @@ class PagePreloader {
const html = await response.text();
this.preloadCache.set(url, html);
this.preloadedPages.add(url);
console.log('🖱️ 호버 프리로드 완료:', url);
}
} catch (error) {
console.warn('호버 프리로드 실패:', url, error);
@@ -285,7 +280,6 @@ class PagePreloader {
if ('serviceWorker' in navigator) {
try {
const registration = await navigator.serviceWorker.register('/sw.js');
console.log('🔧 서비스 워커 등록 완료:', registration);
} catch (error) {
console.log('서비스 워커 등록 실패:', error);
}
@@ -306,7 +300,6 @@ class PagePreloader {
this.preloadCache.clear();
this.resourceCache.clear();
this.preloadedPages.clear();
console.log('🗑️ 프리로드 캐시 정리 완료');
}
}

View File

@@ -47,7 +47,7 @@ class PagePermissionManager {
const match = document.cookie.match(/(?:^|; )sso_token=([^;]*)/);
if (match) return decodeURIComponent(match[1]);
// 3) localStorage 폴백
return localStorage.getItem('sso_token') || localStorage.getItem('access_token');
return localStorage.getItem('sso_token');
}
async loadPagePermissions() {

View File

@@ -47,7 +47,7 @@ async function loadProjects() {
var sel = document.getElementById('projectFilter');
sel.innerHTML = '<option value="">전체 프로젝트</option>';
projects.forEach(function (p) {
sel.innerHTML += '<option value="' + p.id + '">' + p.project_name + '</option>';
sel.innerHTML += '<option value="' + p.id + '">' + escapeHtml(p.project_name) + '</option>';
});
}
} catch (e) { console.error('프로젝트 로드 실패:', e); }

View File

@@ -53,7 +53,7 @@ async function initializeIssueView() {
try {
const user = await AuthAPI.getCurrentUser();
currentUser = user;
localStorage.setItem('currentUser', JSON.stringify(user));
localStorage.setItem('sso_user', JSON.stringify(user));
// 공통 헤더 초기화
await window.commonHeader.init(user, 'issues_view');
@@ -713,7 +713,7 @@ async function handlePasswordChange(e) {
// 현재 사용자 정보도 업데이트
currentUser.password = newPassword;
localStorage.setItem('currentUser', JSON.stringify(currentUser));
localStorage.setItem('sso_user', JSON.stringify(currentUser));
alert('비밀번호가 성공적으로 변경되었습니다.');
document.querySelector('.fixed').remove(); // 모달 닫기

View File

@@ -18,7 +18,7 @@ async function initializeArchive() {
try {
const user = await AuthAPI.getCurrentUser();
currentUser = user;
localStorage.setItem('currentUser', JSON.stringify(user));
localStorage.setItem('sso_user', JSON.stringify(user));
// 공통 헤더 초기화
await window.commonHeader.init(user, 'issues_archive');

View File

@@ -81,7 +81,7 @@ async function initializeInbox() {
try {
const user = await AuthAPI.getCurrentUser();
currentUser = user;
localStorage.setItem('currentUser', JSON.stringify(user));
localStorage.setItem('sso_user', JSON.stringify(user));
// 공통 헤더 초기화
await window.commonHeader.init(user, 'issues_inbox');
@@ -123,7 +123,7 @@ async function initializeInbox() {
// 공통 헤더만이라도 초기화
try {
const user = JSON.parse(localStorage.getItem('currentUser') || '{}');
const user = JSON.parse(localStorage.getItem('sso_user') || '{}');
if (user.id) {
await window.commonHeader.init(user, 'issues_inbox');
// 에러 상황에서도 애니메이션 적용

View File

@@ -27,7 +27,7 @@ async function initializeManagement() {
try {
const user = await AuthAPI.getCurrentUser();
currentUser = user;
localStorage.setItem('currentUser', JSON.stringify(user));
localStorage.setItem('sso_user', JSON.stringify(user));
// 공통 헤더 초기화
await window.commonHeader.init(user, 'issues_management');
@@ -694,9 +694,9 @@ function createCompletedRow(issue, project) {
// 입력 여부 아이콘 생성
function getStatusIcon(value) {
if (value && value.toString().trim() !== '') {
return '<span class="text-green-500 text-lg"></span>';
return '<span class="text-green-500 text-lg"></span>';
} else {
return '<span class="text-gray-400 text-lg"></span>';
return '<span class="text-gray-400 text-lg"></span>';
}
}
@@ -704,9 +704,9 @@ function getStatusIcon(value) {
function getPhotoStatusIcon(photo1, photo2) {
const count = (photo1 ? 1 : 0) + (photo2 ? 1 : 0);
if (count > 0) {
return `<span class="text-green-500 text-lg"></span><span class="text-xs ml-1">${count}장</span>`;
return `<span class="text-green-500 text-lg"></span><span class="text-xs ml-1">${count}장</span>`;
} else {
return '<span class="text-gray-400 text-lg"></span>';
return '<span class="text-gray-400 text-lg"></span>';
}
}
@@ -1194,7 +1194,6 @@ async function saveModalChanges() {
updates[fieldName] = base64;
}
console.log(`📸 ${maxPhotos}장의 완료 사진 처리 완료`);
}
console.log('Modal sending updates:', updates);
@@ -1744,11 +1743,10 @@ async function saveIssueFromModal(issueId) {
const files = completionPhotoElement.files;
const maxPhotos = Math.min(files.length, 5);
console.log(`🔍 총 ${maxPhotos}개의 완료 사진 업로드 시작`);
for (let i = 0; i < maxPhotos; i++) {
const file = files[i];
console.log(`🔍 파일 ${i + 1} 정보:`, {
console.log(` 파일 ${i + 1} 정보:`, {
name: file.name,
size: file.size,
type: file.type
@@ -1760,7 +1758,6 @@ async function saveIssueFromModal(issueId) {
const fieldName = i === 0 ? 'completion_photo' : `completion_photo${i + 1}`;
completionPhotos[fieldName] = base64Data;
console.log(`✅ 파일 ${i + 1} 변환 완료 (${fieldName})`);
}
} catch (error) {
console.error('파일 변환 오류:', error);
@@ -1814,9 +1811,9 @@ async function saveIssueFromModal(issueId) {
if (updatedIssue) {
// 완료 사진이 저장되었는지 확인
if (updatedIssue.completion_photo_path) {
alert(' 완료 사진이 성공적으로 저장되었습니다!');
alert(' 완료 사진이 성공적으로 저장되었습니다!');
} else {
alert('⚠️ 저장은 완료되었지만 완료 사진 저장에 실패했습니다. 다시 시도해주세요.');
alert(' 저장은 완료되었지만 완료 사진 저장에 실패했습니다. 다시 시도해주세요.');
}
// 모달 내용 업데이트 (완료 사진 표시 갱신)
@@ -2075,7 +2072,7 @@ async function saveAndCompleteIssue(issueId) {
if (completionPhotoElement && completionPhotoElement.files[0]) {
try {
const file = completionPhotoElement.files[0];
console.log('🔍 업로드할 파일 정보:', {
console.log(' 업로드할 파일 정보:', {
name: file.name,
size: file.size,
type: file.type,
@@ -2083,12 +2080,8 @@ async function saveAndCompleteIssue(issueId) {
});
const base64 = await fileToBase64(file);
console.log('🔍 Base64 변환 완료 - 전체 길이:', base64.length);
console.log('🔍 Base64 헤더:', base64.substring(0, 50));
completionPhoto = base64.split(',')[1]; // Base64 데이터만 추출
console.log('🔍 헤더 제거 후 길이:', completionPhoto.length);
console.log('🔍 전송할 Base64 시작 부분:', completionPhoto.substring(0, 50));
} catch (error) {
console.error('파일 변환 오류:', error);
alert('완료 사진 업로드 중 오류가 발생했습니다.');

View File

@@ -3,20 +3,28 @@
* M-Project 작업보고서 시스템
*/
const CACHE_NAME = 'mproject-v1.0.3';
const STATIC_CACHE = 'mproject-static-v1.0.3';
const DYNAMIC_CACHE = 'mproject-dynamic-v1.0.3';
const CACHE_NAME = 'mproject-v1.1.0';
const STATIC_CACHE = 'mproject-static-v1.1.0';
const DYNAMIC_CACHE = 'mproject-dynamic-v1.1.0';
// 캐시할 정적 리소스
const STATIC_ASSETS = [
'/',
'/index.html',
'/app.html',
'/issue-view.html',
'/daily-work.html',
'/project-management.html',
'/admin.html',
'/issues-dashboard.html',
'/issues-inbox.html',
'/issues-management.html',
'/issues-archive.html',
'/ai-assistant.html',
'/reports.html',
'/reports-daily.html',
'/reports-weekly.html',
'/reports-monthly.html',
'/static/js/api.js',
'/static/js/app.js',
'/static/js/core/permissions.js',
'/static/js/core/auth-manager.js',
'/static/js/components/common-header.js',
'/static/js/core/page-manager.js',
'/static/js/core/page-preloader.js',
@@ -60,20 +68,20 @@ const CACHE_STRATEGIES = {
* 서비스 워커 설치
*/
self.addEventListener('install', (event) => {
console.log('🔧 서비스 워커 설치 중...');
console.log(' 서비스 워커 설치 중...');
event.waitUntil(
caches.open(STATIC_CACHE)
.then((cache) => {
console.log('📦 정적 리소스 캐싱 중...');
console.log(' 정적 리소스 캐싱 중...');
return cache.addAll(STATIC_ASSETS);
})
.then(() => {
console.log(' 서비스 워커 설치 완료');
console.log(' 서비스 워커 설치 완료');
return self.skipWaiting();
})
.catch((error) => {
console.error(' 서비스 워커 설치 실패:', error);
console.error(' 서비스 워커 설치 실패:', error);
})
);
});
@@ -82,7 +90,7 @@ self.addEventListener('install', (event) => {
* 서비스 워커 활성화
*/
self.addEventListener('activate', (event) => {
console.log('🚀 서비스 워커 활성화 중...');
console.log(' 서비스 워커 활성화 중...');
event.waitUntil(
caches.keys()
@@ -93,14 +101,14 @@ self.addEventListener('activate', (event) => {
if (cacheName !== STATIC_CACHE &&
cacheName !== DYNAMIC_CACHE &&
cacheName !== CACHE_NAME) {
console.log('🗑️ 이전 캐시 삭제:', cacheName);
console.log(' 이전 캐시 삭제:', cacheName);
return caches.delete(cacheName);
}
})
);
})
.then(() => {
console.log(' 서비스 워커 활성화 완료');
console.log(' 서비스 워커 활성화 완료');
return self.clients.claim();
})
);
@@ -251,7 +259,7 @@ function isCDNResource(url) {
async function handleOffline(request) {
// HTML 요청에 대한 오프라인 페이지
if (request.destination === 'document') {
const offlinePage = await caches.match('/index.html');
const offlinePage = await caches.match('/app.html');
if (offlinePage) {
return offlinePage;
}
@@ -308,7 +316,7 @@ async function clearAllCaches() {
await Promise.all(
cacheNames.map(cacheName => caches.delete(cacheName))
);
console.log('🗑️ 모든 캐시 정리 완료');
console.log(' 모든 캐시 정리 완료');
}
/**
@@ -318,7 +326,7 @@ async function cachePage(url) {
try {
const cache = await caches.open(DYNAMIC_CACHE);
await cache.add(url);
console.log('📦 페이지 캐시 완료:', url);
console.log(' 페이지 캐시 완료:', url);
} catch (error) {
console.error('페이지 캐시 실패:', url, error);
}

View File

@@ -92,7 +92,7 @@ async function resetPassword(req, res, next) {
try {
const userId = parseInt(req.params.id);
const { new_password } = req.body;
const password = new_password || '000000';
const password = new_password || process.env.DEFAULT_PASSWORD || 'changeme!1';
const user = await userModel.update(userId, { password });
if (!user) {

View File

@@ -21,8 +21,20 @@ const vacationRoutes = require('./routes/vacationRoutes');
const app = express();
const PORT = process.env.PORT || 3000;
const allowedOrigins = [
'https://tkfb.technicalkorea.net',
'https://tkreport.technicalkorea.net',
'https://tkqc.technicalkorea.net',
'https://tkuser.technicalkorea.net',
];
if (process.env.NODE_ENV === 'development') {
allowedOrigins.push('http://localhost:30080', 'http://localhost:30180', 'http://localhost:30280');
}
app.use(cors({
origin: true,
origin: function(origin, cb) {
if (!origin || allowedOrigins.includes(origin) || /^http:\/\/192\.168\.\d+\.\d+(:\d+)?$/.test(origin)) return cb(null, true);
cb(new Error('CORS blocked: ' + origin));
},
credentials: true
}));
app.use(express.json());

View File

@@ -4,7 +4,7 @@ const API_BASE = '/api';
/* ===== Token ===== */
function _cookieGet(n) { const m = document.cookie.match(new RegExp('(?:^|; )' + n + '=([^;]*)')); return m ? decodeURIComponent(m[1]) : null; }
function _cookieRemove(n) { let c = n + '=; path=/; max-age=0'; if (location.hostname.includes('technicalkorea.net')) c += '; domain=.technicalkorea.net'; document.cookie = c; }
function getToken() { return _cookieGet('sso_token') || localStorage.getItem('sso_token') || localStorage.getItem('access_token'); }
function getToken() { return _cookieGet('sso_token') || localStorage.getItem('sso_token'); }
function getLoginUrl() {
const h = location.hostname;
if (h.includes('technicalkorea.net')) return location.protocol + '//tkfb.technicalkorea.net/login?redirect=' + encodeURIComponent(location.href);
@@ -27,11 +27,14 @@ function showToast(msg, type = 'success') {
document.querySelector('.toast-message')?.remove();
const el = document.createElement('div');
el.className = `toast-message fixed bottom-4 right-4 px-4 py-3 rounded-lg text-white z-[10000] shadow-lg ${type==='success'?'bg-emerald-500':'bg-red-500'}`;
el.innerHTML = `<i class="fas ${type==='success'?'fa-check-circle':'fa-exclamation-circle'} mr-2"></i>${msg}`;
el.innerHTML = `<i class="fas ${type==='success'?'fa-check-circle':'fa-exclamation-circle'} mr-2"></i>${escapeHtml(msg)}`;
document.body.appendChild(el);
setTimeout(() => { el.classList.add('opacity-0'); setTimeout(() => el.remove(), 300); }, 3000);
}
/* ===== Escape ===== */
function escapeHtml(str) { if (!str) return ''; const d = document.createElement('div'); d.textContent = str; return d.innerHTML; }
/* ===== Helpers ===== */
const DEPT_FALLBACK = { production:'생산', quality:'품질', purchasing:'구매', design:'설계', sales:'영업' };
let departmentsCache = [];
@@ -54,7 +57,8 @@ function escHtml(s) { if (!s) return ''; const d = document.createElement('div')
/* ===== Logout ===== */
function doLogout() {
if (!confirm('로그아웃?')) return;
_cookieRemove('sso_token'); localStorage.removeItem('sso_token'); localStorage.removeItem('access_token'); localStorage.removeItem('currentUser');
_cookieRemove('sso_token'); _cookieRemove('sso_user'); _cookieRemove('sso_refresh_token');
['sso_token','sso_user','sso_refresh_token','token','user','access_token','currentUser','current_user','userInfo','userPageAccess'].forEach(k => localStorage.removeItem(k));
location.href = getLoginUrl();
}

View File

@@ -13,7 +13,7 @@ async function loadProjects() {
projectsLoaded = true;
displayProjects();
} catch (err) {
document.getElementById('projectList').innerHTML = `<div class="text-red-500 text-center py-6"><i class="fas fa-exclamation-triangle text-xl"></i><p class="text-sm mt-2">${err.message}</p></div>`;
document.getElementById('projectList').innerHTML = `<div class="text-red-500 text-center py-6"><i class="fas fa-exclamation-triangle text-xl"></i><p class="text-sm mt-2">${escapeHtml(err.message)}</p></div>`;
}
}
@@ -23,18 +23,18 @@ function displayProjects() {
c.innerHTML = projects.map(p => `
<div class="flex items-center justify-between p-2.5 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors">
<div class="flex-1 min-w-0">
<div class="text-sm font-medium text-gray-800 truncate"><i class="fas fa-folder mr-1.5 text-gray-400 text-xs"></i>${p.project_name}</div>
<div class="text-sm font-medium text-gray-800 truncate"><i class="fas fa-folder mr-1.5 text-gray-400 text-xs"></i>${escapeHtml(p.project_name)}</div>
<div class="text-xs text-gray-500 flex items-center gap-1.5 mt-0.5 flex-wrap">
<span class="font-mono">${p.job_no}</span>
${p.site?`<span class="px-1.5 py-0.5 rounded bg-amber-50 text-amber-600">${p.site}</span>`:''}
${p.pm?`<span class="px-1.5 py-0.5 rounded bg-slate-50 text-slate-500">${p.pm}</span>`:''}
<span class="font-mono">${escapeHtml(p.job_no)}</span>
${p.site?`<span class="px-1.5 py-0.5 rounded bg-amber-50 text-amber-600">${escapeHtml(p.site)}</span>`:''}
${p.pm?`<span class="px-1.5 py-0.5 rounded bg-slate-50 text-slate-500">${escapeHtml(p.pm)}</span>`:''}
${statusBadge(p.project_status, p.is_active)}
${p.due_date?`<span class="text-gray-400">${formatDate(p.due_date)}</span>`:''}
</div>
</div>
<div class="flex gap-1 ml-2 flex-shrink-0">
<button onclick="editProject(${p.project_id})" class="p-1.5 text-slate-500 hover:text-slate-700 hover:bg-slate-200 rounded" title="편집"><i class="fas fa-pen-to-square text-xs"></i></button>
${p.is_active?`<button onclick="deactivateProject(${p.project_id},'${p.project_name.replace(/'/g,"\\'")}')" class="p-1.5 text-red-400 hover:text-red-600 hover:bg-red-100 rounded" title="비활성화"><i class="fas fa-ban text-xs"></i></button>`:''}
${p.is_active?`<button onclick="deactivateProject(${p.project_id},'${escapeHtml(p.project_name).replace(/'/g,"\\'")}')" class="p-1.5 text-red-400 hover:text-red-600 hover:bg-red-100 rounded" title="비활성화"><i class="fas fa-ban text-xs"></i></button>`:''}
</div>
</div>`).join('');
}

View File

@@ -62,7 +62,7 @@ function populateTaskWorkTypeSelect() {
const val = sel.value;
sel.innerHTML = '<option value="">미지정</option>';
taskWorkTypes.forEach(wt => {
sel.innerHTML += `<option value="${wt.id}">${wt.category ? wt.category + ' > ' : ''}${wt.name}</option>`;
sel.innerHTML += `<option value="${wt.id}">${wt.category ? escapeHtml(wt.category) + ' > ' : ''}${escapeHtml(wt.name)}</option>`;
});
sel.value = val;
}

View File

@@ -246,11 +246,11 @@ function openVacBalanceModal(editId) {
// 작업자 셀렉트
const wSel = document.getElementById('vbWorker');
wSel.innerHTML = '<option value="">선택</option>';
vacWorkers.forEach(w => { wSel.innerHTML += `<option value="${w.worker_id}">${w.worker_name}</option>`; });
vacWorkers.forEach(w => { wSel.innerHTML += `<option value="${w.worker_id}">${escapeHtml(w.worker_name)}</option>`; });
// 유형 셀렉트
const tSel = document.getElementById('vbType');
tSel.innerHTML = '<option value="">선택</option>';
vacTypes.filter(t => t.is_active).forEach(t => { tSel.innerHTML += `<option value="${t.id}">${t.type_name} (${t.type_code})</option>`; });
vacTypes.filter(t => t.is_active).forEach(t => { tSel.innerHTML += `<option value="${t.id}">${escapeHtml(t.type_name)} (${escapeHtml(t.type_code)})</option>`; });
if (editId) {
const b = vacBalances.find(x => x.id === editId);
if (!b) return;