diff --git a/system2-report/api/controllers/workIssueController.js b/system2-report/api/controllers/workIssueController.js index e89a5a8..96b822a 100644 --- a/system2-report/api/controllers/workIssueController.js +++ b/system2-report/api/controllers/workIssueController.js @@ -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) => { - 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: '카테고리 조회 실패' }); +exports.getCategoriesByType = async (req, res) => { + try { + const { type } = req.params; + if (!['nonconformity', 'safety', 'facility'].includes(type)) { + return res.status(400).json({ success: false, error: '유효하지 않은 카테고리 타입입니다.' }); } + const categories = await workIssueModel.getCategoriesByType(type); res.json({ success: true, data: categories }); - }); -}; - -/** - * 카테고리 생성 - */ -exports.createCategory = (req, res) => { - const { category_type, category_name, description, display_order } = req.body; - - if (!category_type || !category_name) { - return res.status(400).json({ success: false, error: '카테고리 타입과 이름은 필수입니다.' }); + } catch (err) { + console.error('카테고리 조회 실패:', err); + res.status(500).json({ success: false, error: '카테고리 조회 실패' }); } - - workIssueModel.createCategory( - { category_type, category_name, description, display_order }, - (err, categoryId) => { - if (err) { - console.error('카테고리 생성 실패:', err); - return res.status(500).json({ success: false, error: '카테고리 생성 실패' }); - } - res.status(201).json({ - success: true, - message: '카테고리가 생성되었습니다.', - data: { category_id: categoryId } - }); - } - ); }; -/** - * 카테고리 수정 - */ -exports.updateCategory = (req, res) => { - 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: '카테고리 수정 실패' }); - } - res.json({ success: true, message: '카테고리가 수정되었습니다.' }); +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: '카테고리 타입과 이름은 필수입니다.' }); } - ); + 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); + res.status(500).json({ success: false, error: '카테고리 생성 실패' }); + } }; -/** - * 카테고리 삭제 - */ -exports.deleteCategory = (req, res) => { - const { id } = req.params; +exports.updateCategory = async (req, res) => { + try { + const { id } = req.params; + const { category_name, description, display_order, is_active } = req.body; + 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: '카테고리 수정 실패' }); + } +}; - workIssueModel.deleteCategory(id, (err, result) => { - if (err) { - console.error('카테고리 삭제 실패:', err); - return res.status(500).json({ success: false, error: '카테고리 삭제 실패' }); - } +exports.deleteCategory = async (req, res) => { + try { + const { id } = req.params; + 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) => { - const { categoryId } = req.params; - - workIssueModel.getItemsByCategory(categoryId, (err, items) => { - if (err) { - console.error('항목 조회 실패:', err); - return res.status(500).json({ success: false, error: '항목 조회 실패' }); - } +exports.getItemsByCategory = async (req, res) => { + try { + const { categoryId } = req.params; + const items = await workIssueModel.getItemsByCategory(categoryId); res.json({ success: true, data: items }); - }); -}; - -/** - * 모든 항목 조회 - */ -exports.getAllItems = (req, res) => { - workIssueModel.getAllItems((err, items) => { - if (err) { - console.error('항목 조회 실패:', err); - return res.status(500).json({ success: false, error: '항목 조회 실패' }); - } - res.json({ success: true, data: items }); - }); -}; - -/** - * 항목 생성 - */ -exports.createItem = (req, res) => { - 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와 항목명은 필수입니다.' }); + } catch (err) { + console.error('항목 조회 실패:', err); + res.status(500).json({ success: false, error: '항목 조회 실패' }); } - - workIssueModel.createItem( - { category_id, item_name, description, severity, display_order }, - (err, itemId) => { - if (err) { - console.error('항목 생성 실패:', err); - return res.status(500).json({ success: false, error: '항목 생성 실패' }); - } - res.status(201).json({ - success: true, - message: '항목이 생성되었습니다.', - data: { item_id: itemId } - }); - } - ); }; -/** - * 항목 수정 - */ -exports.updateItem = (req, res) => { - 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: '항목 수정 실패' }); - } - res.json({ success: true, message: '항목이 수정되었습니다.' }); - } - ); +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.deleteItem = (req, res) => { - const { id } = req.params; - - workIssueModel.deleteItem(id, (err, result) => { - if (err) { - console.error('항목 삭제 실패:', err); - return res.status(500).json({ success: false, error: '항목 삭제 실패' }); +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와 항목명은 필수입니다.' }); } + 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); + res.status(500).json({ success: false, error: '항목 생성 실패' }); + } +}; + +exports.updateItem = async (req, res) => { + try { + const { id } = req.params; + const { item_name, description, severity, display_order, is_active } = req.body; + 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 = async (req, res) => { + try { + const { id } = req.params; + 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( - { - 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); - } - ); - }); - } catch (itemErr) { - console.error('커스텀 항목 생성 실패:', itemErr); - return res.status(500).json({ success: false, error: '항목 저장 실패' }); - } + finalItemId = await workIssueModel.createItem({ + category_id: issue_category_id, + item_name: custom_item_name, + description: '사용자 직접 입력', + severity: 'medium', + display_order: 999 + }); } - // 사진 저장 (최대 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,143 +189,106 @@ 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: '문제 신고가 등록되었습니다.', - data: { report_id: reportId } - }); + res.status(201).json({ + success: true, + message: '문제 신고가 등록되었습니다.', + data: { report_id: reportId } + }); - // 부적합 유형이면 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에 전달 - const fs = require('fs').promises; - const path = require('path'); - const photoBase64List = []; - for (const p of Object.values(photoPaths)) { - if (!p) continue; - 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); - } 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); - }); - }); - if (locationParts) { - locationInfo = locationParts.factory_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, - reporter_name: req.user.name || req.user.username, - tk_issue_id: reportId, - project_id: project_id || null, - location_info: locationInfo, - photos: photoBase64List, - ssoToken: originalToken - }); - if (result.success && result.mProjectId) { - workIssueModel.updateMProjectId(reportId, result.mProjectId, () => {}); + // 부적합 유형이면 System 3(tkqc)으로 비동기 전달 + try { + if (catInfo && catInfo.category_type === 'nonconformity') { + const fs = require('fs').promises; + const path = require('path'); + const photoBase64List = []; + for (const p of Object.values(photoPaths)) { + if (!p) continue; + try { + const filePath = path.join(__dirname, '..', p); + const buf = await fs.readFile(filePath); + photoBase64List.push(`data:image/jpeg;base64,${buf.toString('base64')}`); + } catch (readErr) { + console.error('사진 파일 읽기 실패:', p, readErr.message); } } - } catch (e) { - console.error('System3 연동 실패 (신고는 정상 저장됨):', e.message); + + let locationInfo = custom_location || null; + if (factory_category_id) { + try { + const locationParts = await workIssueModel.getLocationNames(factory_category_id, workplace_id); + if (locationParts) { + locationInfo = locationParts.factory_name || ''; + if (locationParts.workplace_name) locationInfo += ` - ${locationParts.workplace_name}`; + } + } catch (locErr) { + console.error('위치 정보 조회 실패:', locErr.message); + } + } + + const originalToken = (req.headers['authorization'] || '').replace('Bearer ', ''); + const result = await mProjectService.sendToMProject({ + 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, + location_info: locationInfo, + photos: photoBase64List, + ssoToken: originalToken + }); + if (result.success && 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) => { - const filters = { - status: req.query.status, - category_type: req.query.category_type, - issue_category_id: req.query.issue_category_id, - factory_category_id: req.query.factory_category_id, - workplace_id: req.query.workplace_id, - assigned_user_id: req.query.assigned_user_id, - start_date: req.query.start_date, - end_date: req.query.end_date, - search: req.query.search, - limit: req.query.limit, - offset: req.query.offset - }; +exports.getAllReports = async (req, res) => { + try { + const filters = { + status: req.query.status, + category_type: req.query.category_type, + issue_category_id: req.query.issue_category_id, + factory_category_id: req.query.factory_category_id, + workplace_id: req.query.workplace_id, + assigned_user_id: req.query.assigned_user_id, + start_date: req.query.start_date, + end_date: req.query.end_date, + search: req.query.search, + limit: req.query.limit, + 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 userLevel = req.user.access_level; + if (!['admin', 'system', 'support_team'].includes(userLevel)) { + filters.reporter_id = req.user.user_id; } + + 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) => { - const { id } = req.params; - - workIssueModel.getReportById(id, (err, report) => { - if (err) { - console.error('신고 상세 조회 실패:', err); - return res.status(500).json({ success: false, error: '신고 상세 조회 실패' }); - } +exports.getReportById = async (req, res) => { + try { + const { id } = req.params; + 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,109 +299,66 @@ 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; + const report = await workIssueModel.getReportById(id); - // 기존 신고 확인 - workIssueModel.getReportById(id, async (err, report) => { - if (err) { - console.error('신고 조회 실패:', err); - return res.status(500).json({ success: false, error: '신고 조회 실패' }); + 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); + + if (!isOwner && !isManager) { + return res.status(403).json({ success: false, error: '수정 권한이 없습니다.' }); + } + 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 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); + const savedPath = await imageUploadService.saveBase64Image(photos[i], 'issue'); + if (savedPath) photoPaths[`photo_path${i + 1}`] = savedPath; } + } - if (!report) { - return res.status(404).json({ success: false, error: '신고를 찾을 수 없습니다.' }); - } + await workIssueModel.updateReport(id, { + factory_category_id, workplace_id, custom_location, + issue_category_id, issue_item_id, additional_description, + ...photoPaths + }, req.user.user_id); - // 권한 확인 - const userLevel = req.user.access_level; - const isOwner = report.reporter_id === req.user.user_id; - const isManager = ['admin', 'system'].includes(userLevel); - - 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 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); - } - // 새 사진 저장 - const savedPath = await imageUploadService.saveBase64Image(photos[i], 'issue'); - 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, - ...photoPaths - }; - - 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: '신고가 수정되었습니다.' }); - }); - }); + res.json({ success: true, message: '신고가 수정되었습니다.' }); } catch (error) { console.error('신고 수정 에러:', error); res.status(500).json({ success: false, error: '서버 오류가 발생했습니다.' }); } }; -/** - * 신고 삭제 - */ exports.deleteReport = async (req, res) => { - const { id } = req.params; - - workIssueModel.getReportById(id, async (err, report) => { - if (err) { - console.error('신고 조회 실패:', err); - return res.status(500).json({ success: false, error: '신고 조회 실패' }); - } + try { + const { id } = req.params; + 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,229 +367,163 @@ 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, - photos.photo_path4, photos.photo_path5, - photos.resolution_photo_path1, photos.resolution_photo_path2 - ].filter(Boolean); - await imageUploadService.deleteMultipleFiles(allPhotos); - } + if (photos) { + const allPhotos = [ + photos.photo_path1, photos.photo_path2, photos.photo_path3, + photos.photo_path4, photos.photo_path5, + photos.resolution_photo_path1, photos.resolution_photo_path2 + ].filter(Boolean); + await imageUploadService.deleteMultipleFiles(allPhotos); + } - res.json({ success: true, message: '신고가 삭제되었습니다.' }); - }); - }); + res.json({ success: true, message: '신고가 삭제되었습니다.' }); + } catch (err) { + console.error('신고 삭제 실패:', err); + res.status(500).json({ success: false, error: '신고 삭제 실패' }); + } }; // ==================== 상태 관리 ==================== -/** - * 신고 접수 - */ -exports.receiveReport = (req, res) => { - 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 || '신고 접수 실패' }); - } +exports.receiveReport = async (req, res) => { + try { + const { id } = req.params; + await workIssueModel.receiveReport(id, req.user.user_id); res.json({ success: true, message: '신고가 접수되었습니다.' }); - }); -}; - -/** - * 담당자 배정 - */ -exports.assignReport = (req, res) => { - const { id } = req.params; - const { assigned_department, assigned_user_id } = req.body; - - if (!assigned_user_id) { - return res.status(400).json({ success: false, error: '담당자는 필수입니다.' }); + } catch (err) { + console.error('신고 접수 실패:', err); + res.status(400).json({ success: false, error: err.message || '신고 접수 실패' }); } +}; - 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 || '담당자 배정 실패' }); +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: '담당자는 필수입니다.' }); } + await workIssueModel.assignReport(id, { assigned_department, assigned_user_id, assigned_by: req.user.user_id }); res.json({ success: true, message: '담당자가 배정되었습니다.' }); - }); + } catch (err) { + console.error('담당자 배정 실패:', err); + res.status(400).json({ success: false, error: err.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 || '처리 시작 실패' }); - } +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 || '처리 시작 실패' }); + } }; -/** - * 처리 완료 - */ 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 || '처리 완료 실패' }); - } - res.json({ success: true, message: '처리가 완료되었습니다.' }); - }); - } catch (error) { - console.error('처리 완료 에러:', error); - res.status(500).json({ success: false, error: '서버 오류가 발생했습니다.' }); + await workIssueModel.completeReport(id, { resolution_notes, resolution_photo_path1, resolution_photo_path2, resolved_by: req.user.user_id }); + res.json({ success: true, message: '처리가 완료되었습니다.' }); + } catch (err) { + console.error('처리 완료 실패:', err); + res.status(400).json({ success: false, error: err.message || '처리 완료 실패' }); } }; -/** - * 신고 종료 - */ -exports.closeReport = (req, res) => { - 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 || '신고 종료 실패' }); - } +exports.closeReport = async (req, res) => { + try { + const { id } = req.params; + 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) => { - const { id } = req.params; - - workIssueModel.getStatusLogs(id, (err, logs) => { - if (err) { - console.error('상태 이력 조회 실패:', err); - return res.status(500).json({ success: false, error: '상태 이력 조회 실패' }); - } +exports.getStatusLogs = async (req, res) => { + try { + const { id } = req.params; + 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) => { - const { id } = req.params; - const { category_type } = req.body; +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 || '유형 이관 실패' }); + const validTypes = ['nonconformity', 'safety', 'facility']; + if (!category_type || !validTypes.includes(category_type)) { + return res.status(400).json({ success: false, error: '유효하지 않은 유형입니다. (nonconformity, safety, facility)' }); } + + 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) => { - 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) { - console.error('통계 조회 실패:', err); - return res.status(500).json({ success: false, error: '통계 조회 실패' }); - } +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 + }; + const stats = await workIssueModel.getStatsSummary(filters); res.json({ success: true, data: stats }); - }); + } catch (err) { + console.error('통계 조회 실패:', err); + res.status(500).json({ success: false, error: '통계 조회 실패' }); + } }; -/** - * 카테고리별 통계 - */ -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) { - console.error('카테고리별 통계 조회 실패:', err); - return res.status(500).json({ success: false, error: '통계 조회 실패' }); - } +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); + res.status(500).json({ success: false, error: '통계 조회 실패' }); + } }; -/** - * 작업장별 통계 - */ -exports.getStatsByWorkplace = (req, res) => { - 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: '통계 조회 실패' }); - } +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 + }; + const stats = await workIssueModel.getStatsByWorkplace(filters); res.json({ success: true, data: stats }); - }); + } catch (err) { + console.error('작업장별 통계 조회 실패:', err); + res.status(500).json({ success: false, error: '통계 조회 실패' }); + } }; diff --git a/system2-report/api/controllers/workIssueModel.js b/system2-report/api/controllers/workIssueModel.js deleted file mode 100644 index e25183d..0000000 --- a/system2-report/api/controllers/workIssueModel.js +++ /dev/null @@ -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 -}; diff --git a/system2-report/api/index.js b/system2-report/api/index.js index 6f270e7..0b95999 100644 --- a/system2-report/api/index.js +++ b/system2-report/api/index.js @@ -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 })); diff --git a/system2-report/api/models/workIssueModel.js b/system2-report/api/models/workIssueModel.js index 9e9779f..c01c3a6 100644 --- a/system2-report/api/models/workIssueModel.js +++ b/system2-report/api/models/workIssueModel.js @@ -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 = { diff --git a/system2-report/web/js/api-base.js b/system2-report/web/js/api-base.js index 87c82cb..b0ca2b5 100644 --- a/system2-report/web/js/api-base.js +++ b/system2-report/web/js/api-base.js @@ -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 방지) ==================== diff --git a/system3-nonconformance/api/main.py b/system3-nonconformance/api/main.py index 4f835e4..7aa01ba 100644 --- a/system3-nonconformance/api/main.py +++ b/system3-nonconformance/api/main.py @@ -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=["*"] diff --git a/system3-nonconformance/web/static/js/api.js b/system3-nonconformance/web/static/js/api.js index 60c2bf5..76efc53 100644 --- a/system3-nonconformance/web/static/js/api.js +++ b/system3-nonconformance/web/static/js/api.js @@ -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'); } }; diff --git a/system3-nonconformance/web/static/js/app.js b/system3-nonconformance/web/static/js/app.js index 9b11f4a..a6b8290 100644 --- a/system3-nonconformance/web/static/js/app.js +++ b/system3-nonconformance/web/static/js/app.js @@ -47,22 +47,16 @@ 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('사용자 정보 없음'); - } + throw new Error('사용자 정보 없음'); } _cookieGet(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(); } diff --git a/system3-nonconformance/web/static/js/components/common-header.js b/system3-nonconformance/web/static/js/components/common-header.js index 9911a1e..28d9566 100644 --- a/system3-nonconformance/web/static/js/components/common-header.js +++ b/system3-nonconformance/web/static/js/components/common-header.js @@ -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); // 권한 시스템 로드 후 실행 } } diff --git a/system3-nonconformance/web/static/js/core/auth-manager.js b/system3-nonconformance/web/static/js/core/auth-manager.js index aa72b69..48b5afb 100644 --- a/system3-nonconformance/web/static/js/core/auth-manager.js +++ b/system3-nonconformance/web/static/js/core/auth-manager.js @@ -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 로드 완료'); diff --git a/system3-nonconformance/web/static/js/core/keyboard-shortcuts.js b/system3-nonconformance/web/static/js/core/keyboard-shortcuts.js index 74ebec2..c1cf356 100644 --- a/system3-nonconformance/web/static/js/core/keyboard-shortcuts.js +++ b/system3-nonconformance/web/static/js/core/keyboard-shortcuts.js @@ -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 ? '활성화' : '비활성화'}`); } /** diff --git a/system3-nonconformance/web/static/js/core/page-manager.js b/system3-nonconformance/web/static/js/core/page-manager.js index 7a21b21..fcdaeb8 100644 --- a/system3-nonconformance/web/static/js/core/page-manager.js +++ b/system3-nonconformance/web/static/js/core/page-manager.js @@ -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; } diff --git a/system3-nonconformance/web/static/js/core/page-preloader.js b/system3-nonconformance/web/static/js/core/page-preloader.js index d085de2..486d02b 100644 --- a/system3-nonconformance/web/static/js/core/page-preloader.js +++ b/system3-nonconformance/web/static/js/core/page-preloader.js @@ -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('🗑️ 프리로드 캐시 정리 완료'); } } diff --git a/system3-nonconformance/web/static/js/core/permissions.js b/system3-nonconformance/web/static/js/core/permissions.js index 3a2f974..6b0b0d7 100644 --- a/system3-nonconformance/web/static/js/core/permissions.js +++ b/system3-nonconformance/web/static/js/core/permissions.js @@ -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() { diff --git a/system3-nonconformance/web/static/js/m/m-dashboard.js b/system3-nonconformance/web/static/js/m/m-dashboard.js index 83c2826..5f08f8e 100644 --- a/system3-nonconformance/web/static/js/m/m-dashboard.js +++ b/system3-nonconformance/web/static/js/m/m-dashboard.js @@ -47,7 +47,7 @@ async function loadProjects() { var sel = document.getElementById('projectFilter'); sel.innerHTML = ''; projects.forEach(function (p) { - sel.innerHTML += ''; + sel.innerHTML += ''; }); } } catch (e) { console.error('프로젝트 로드 실패:', e); } diff --git a/system3-nonconformance/web/static/js/pages/issue-view.js b/system3-nonconformance/web/static/js/pages/issue-view.js index 6bdf11f..9a26b90 100644 --- a/system3-nonconformance/web/static/js/pages/issue-view.js +++ b/system3-nonconformance/web/static/js/pages/issue-view.js @@ -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(); // 모달 닫기 diff --git a/system3-nonconformance/web/static/js/pages/issues-archive.js b/system3-nonconformance/web/static/js/pages/issues-archive.js index 025bcb0..ed49adc 100644 --- a/system3-nonconformance/web/static/js/pages/issues-archive.js +++ b/system3-nonconformance/web/static/js/pages/issues-archive.js @@ -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'); diff --git a/system3-nonconformance/web/static/js/pages/issues-inbox.js b/system3-nonconformance/web/static/js/pages/issues-inbox.js index f58b2a4..32c6c84 100644 --- a/system3-nonconformance/web/static/js/pages/issues-inbox.js +++ b/system3-nonconformance/web/static/js/pages/issues-inbox.js @@ -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'); // 에러 상황에서도 애니메이션 적용 diff --git a/system3-nonconformance/web/static/js/pages/issues-management.js b/system3-nonconformance/web/static/js/pages/issues-management.js index 51087b7..cdec667 100644 --- a/system3-nonconformance/web/static/js/pages/issues-management.js +++ b/system3-nonconformance/web/static/js/pages/issues-management.js @@ -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 '✅'; + return ''; } else { - return '❌'; + return ''; } } @@ -704,9 +704,9 @@ function getStatusIcon(value) { function getPhotoStatusIcon(photo1, photo2) { const count = (photo1 ? 1 : 0) + (photo2 ? 1 : 0); if (count > 0) { - return `✅${count}장`; + return `${count}장`; } else { - return '❌'; + return ''; } } @@ -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('완료 사진 업로드 중 오류가 발생했습니다.'); diff --git a/system3-nonconformance/web/sw.js b/system3-nonconformance/web/sw.js index 09b77ae..eca27e4 100644 --- a/system3-nonconformance/web/sw.js +++ b/system3-nonconformance/web/sw.js @@ -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); } diff --git a/user-management/api/controllers/userController.js b/user-management/api/controllers/userController.js index d4eb0fb..848744e 100644 --- a/user-management/api/controllers/userController.js +++ b/user-management/api/controllers/userController.js @@ -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) { diff --git a/user-management/api/index.js b/user-management/api/index.js index 00f28ea..4ded40c 100644 --- a/user-management/api/index.js +++ b/user-management/api/index.js @@ -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()); diff --git a/user-management/web/static/js/tkuser-core.js b/user-management/web/static/js/tkuser-core.js index 479c004..258b76e 100644 --- a/user-management/web/static/js/tkuser-core.js +++ b/user-management/web/static/js/tkuser-core.js @@ -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 = `${msg}`; + el.innerHTML = `${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(); } diff --git a/user-management/web/static/js/tkuser-projects.js b/user-management/web/static/js/tkuser-projects.js index 8a3cac8..6693d3d 100644 --- a/user-management/web/static/js/tkuser-projects.js +++ b/user-management/web/static/js/tkuser-projects.js @@ -13,7 +13,7 @@ async function loadProjects() { projectsLoaded = true; displayProjects(); } catch (err) { - document.getElementById('projectList').innerHTML = `
${err.message}
${escapeHtml(err.message)}