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:
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
};
|
||||
@@ -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
|
||||
}));
|
||||
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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 방지) ====================
|
||||
|
||||
@@ -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=["*"]
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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); // 권한 시스템 로드 후 실행
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 로드 완료');
|
||||
|
||||
@@ -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 ? '활성화' : '비활성화'}`);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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('🗑️ 프리로드 캐시 정리 완료');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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); }
|
||||
|
||||
@@ -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(); // 모달 닫기
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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');
|
||||
// 에러 상황에서도 애니메이션 적용
|
||||
|
||||
@@ -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('완료 사진 업로드 중 오류가 발생했습니다.');
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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('');
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user