feat: 모바일 신고 시스템 구축 + tkqc 연동 + tkuser 이슈유형 관리

- tkreport 모바일 신고 페이지 (5단계 위자드: 유형→위치→프로젝트→항목→사진)
- 프로젝트 DB 연동 (아코디언 UI: TBM등록/활성프로젝트/모름)
- 클라이언트 이미지 리사이징 (1280px, JPEG 80%)
- nginx client_max_body_size 50m, /api/projects/ 프록시 추가
- 부적합 신고 → tkqc 자동 연동 (사진 base64 전달, SSO 토큰 유지)
- work_issue_reports에 project_id 컬럼 추가
- imageUploadService 경로 수정 (public/uploads → uploads, Docker 볼륨 일치)
- tkuser 이슈유형 탭, 휴가관리, nginx 프록시 업데이트
- tkqc 대시보드/수신함/관리함/폐기함 UI 업데이트
- system1 랜딩페이지 업데이트

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Hyungi Ahn
2026-02-12 15:52:45 +09:00
parent 733bb0cb35
commit 234a6252c0
18 changed files with 1308 additions and 1208 deletions

View File

@@ -208,6 +208,7 @@ exports.createReport = async (req, res) => {
issue_category_id,
issue_item_id,
custom_item_name, // 직접 입력한 항목명
project_id,
additional_description,
photos = []
} = req.body;
@@ -275,6 +276,7 @@ exports.createReport = async (req, res) => {
reporter_id,
factory_category_id: factory_category_id || null,
workplace_id: workplace_id || null,
project_id: project_id || null,
custom_location: custom_location || null,
tbm_session_id: tbm_session_id || null,
visit_request_id: visit_request_id || null,
@@ -306,23 +308,35 @@ exports.createReport = async (req, res) => {
});
if (categoryInfo && categoryInfo.category_type === 'nonconformity') {
// 사진은 System 2에만 저장, URL 참조만 전달
const baseUrl = process.env.SYSTEM2_PUBLIC_URL || 'https://tkreport.technicalkorea.net';
const photoUrls = Object.values(photoPaths).filter(Boolean)
.map(p => `${baseUrl}/api/uploads/${p}`);
const descParts = [additional_description || categoryInfo.category_name];
if (photoUrls.length > 0) {
descParts.push('', '[첨부 사진]');
photoUrls.forEach((url, i) => descParts.push(`${i + 1}. ${url}`));
// 저장된 사진 파일을 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;
// 원래 신고자의 SSO 토큰 추출
const originalToken = (req.headers['authorization'] || '').replace('Bearer ', '');
const result = await mProjectService.sendToMProject({
category: categoryInfo.category_name,
description: descParts.join('\n'),
description: descText,
reporter_name: req.user.name || req.user.username,
tk_issue_id: reportId,
photos: [] // 사진 복사 안 함 (URL 참조만)
project_id: project_id || null,
photos: photoBase64List,
ssoToken: originalToken
});
if (result.success && result.mProjectId) {
workIssueModel.updateMProjectId(reportId, result.mProjectId, () => {});

View File

@@ -231,6 +231,7 @@ const createReport = async (reportData, callback) => {
reporter_id,
factory_category_id = null,
workplace_id = null,
project_id = null,
custom_location = null,
tbm_session_id = null,
visit_request_id = null,
@@ -249,11 +250,11 @@ const createReport = async (reportData, callback) => {
const [result] = await db.query(
`INSERT INTO work_issue_reports
(reporter_id, report_date, factory_category_id, workplace_id, custom_location,
(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, custom_location,
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]
);

View File

@@ -21,8 +21,8 @@ try {
// 업로드 디렉토리 설정
const UPLOAD_DIRS = {
issues: path.join(__dirname, '../public/uploads/issues'),
equipments: path.join(__dirname, '../public/uploads/equipments')
issues: path.join(__dirname, '../uploads/issues'),
equipments: path.join(__dirname, '../uploads/equipments')
};
const UPLOAD_DIR = UPLOAD_DIRS.issues; // 기존 호환성 유지
const MAX_SIZE = { width: 1920, height: 1920 };

View File

@@ -184,14 +184,21 @@ async function sendToMProject(issueData) {
description,
reporter_name,
project_name,
project_id = null,
tk_issue_id,
photos = [],
ssoToken = null,
} = issueData;
logger.info('M-Project 연동 시작', { tk_issue_id, category });
// 인증 토큰 획득
const token = await getAuthToken();
// SSO 토큰이 있으면 원래 사용자로 전송, 없으면 api_service 토큰
let token;
if (ssoToken) {
token = ssoToken;
} else {
token = await getAuthToken();
}
if (!token) {
return { success: false, error: 'M-Project 인증 실패' };
}
@@ -219,7 +226,7 @@ async function sendToMProject(issueData) {
const requestBody = {
category: mProjectCategory,
description: enhancedDescription,
project_id: M_PROJECT_CONFIG.defaultProjectId,
project_id: project_id || M_PROJECT_CONFIG.defaultProjectId,
};
// 사진 추가