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

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

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -47,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();
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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