Files
tk-factory-services/system1-factory/api/models/equipmentModel.js
Hyungi Ahn 84cf222b81 feat(tkuser): 알림 시스템 이관 system1-factory → tkuser
- Phase 1: tkuser에 알림 CRUD, Push/ntfy 발송, 내부 알림 API 추가
- Phase 2: notifyHelper URL을 tkuser-api:3000으로 전환 (system2, tkpurchase, tksafety, system1)
- Phase 3: notification-bell.js API 도메인 tkuser로 변경 + 캐시 버스팅 v=4
- Phase 4: system1에서 알림 코드 제거 (routes, controllers, models, utils)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 15:56:41 +09:00

764 lines
22 KiB
JavaScript

// models/equipmentModel.js
const { getDb } = require('../dbPool');
const notifyHelper = require('../utils/notifyHelper');
const EquipmentModel = {
// CREATE - 설비 생성
create: async (equipmentData) => {
const db = await getDb();
const query = `
INSERT INTO equipments (
equipment_code, equipment_name, equipment_type, model_name,
manufacturer, supplier, purchase_price, installation_date, serial_number, specifications,
status, notes, workplace_id, map_x_percent, map_y_percent,
map_width_percent, map_height_percent
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`;
const values = [
equipmentData.equipment_code,
equipmentData.equipment_name,
equipmentData.equipment_type || null,
equipmentData.model_name || null,
equipmentData.manufacturer || null,
equipmentData.supplier || null,
equipmentData.purchase_price || null,
equipmentData.installation_date || null,
equipmentData.serial_number || null,
equipmentData.specifications || null,
equipmentData.status || 'active',
equipmentData.notes || null,
equipmentData.workplace_id || null,
equipmentData.map_x_percent || null,
equipmentData.map_y_percent || null,
equipmentData.map_width_percent || null,
equipmentData.map_height_percent || null
];
const [result] = await db.query(query, values);
return {
equipment_id: result.insertId,
...equipmentData
};
},
// READ ALL - 모든 설비 조회 (필터링 옵션 포함)
getAll: async (filters) => {
const db = await getDb();
let query = `
SELECT
e.*,
w.workplace_name,
wc.category_name
FROM equipments e
LEFT JOIN workplaces w ON e.workplace_id = w.workplace_id
LEFT JOIN workplace_categories wc ON w.category_id = wc.category_id
WHERE 1=1
`;
const values = [];
if (filters.workplace_id) {
query += ' AND e.workplace_id = ?';
values.push(filters.workplace_id);
}
if (filters.equipment_type) {
query += ' AND e.equipment_type = ?';
values.push(filters.equipment_type);
}
if (filters.status) {
query += ' AND e.status = ?';
values.push(filters.status);
}
if (filters.search) {
query += ' AND (e.equipment_name LIKE ? OR e.equipment_code LIKE ?)';
const searchTerm = `%${filters.search}%`;
values.push(searchTerm, searchTerm);
}
query += ' ORDER BY e.equipment_code ASC';
const [rows] = await db.query(query, values);
return rows;
},
// READ ONE - 특정 설비 조회
getById: async (equipmentId) => {
const db = await getDb();
const [rows] = await db.query(
`SELECT
e.*,
w.workplace_name,
wc.category_name
FROM equipments e
LEFT JOIN workplaces w ON e.workplace_id = w.workplace_id
LEFT JOIN workplace_categories wc ON w.category_id = wc.category_id
WHERE e.equipment_id = ?`,
[equipmentId]
);
return rows[0];
},
// READ BY WORKPLACE - 특정 작업장의 설비 조회
getByWorkplace: async (workplaceId) => {
const db = await getDb();
const [rows] = await db.query(
`SELECT e.*
FROM equipments e
WHERE e.workplace_id = ?
ORDER BY e.equipment_code ASC`,
[workplaceId]
);
return rows;
},
// READ ACTIVE - 활성 설비만 조회
getActive: async () => {
const db = await getDb();
const [rows] = await db.query(
`SELECT
e.*,
w.workplace_name,
wc.category_name
FROM equipments e
LEFT JOIN workplaces w ON e.workplace_id = w.workplace_id
LEFT JOIN workplace_categories wc ON w.category_id = wc.category_id
WHERE e.status = 'active'
ORDER BY e.equipment_code ASC`
);
return rows;
},
// UPDATE - 설비 수정
update: async (equipmentId, equipmentData) => {
const db = await getDb();
const values = [
equipmentData.equipment_code,
equipmentData.equipment_name,
equipmentData.equipment_type || null,
equipmentData.model_name || null,
equipmentData.manufacturer || null,
equipmentData.supplier || null,
equipmentData.purchase_price || null,
equipmentData.installation_date || null,
equipmentData.serial_number || null,
equipmentData.specifications || null,
equipmentData.status || 'active',
equipmentData.notes || null,
equipmentData.workplace_id || null,
equipmentData.map_x_percent || null,
equipmentData.map_y_percent || null,
equipmentData.map_width_percent || null,
equipmentData.map_height_percent || null,
equipmentId
];
const [result] = await db.query(
`UPDATE equipments SET
equipment_code = ?,
equipment_name = ?,
equipment_type = ?,
model_name = ?,
manufacturer = ?,
supplier = ?,
purchase_price = ?,
installation_date = ?,
serial_number = ?,
specifications = ?,
status = ?,
notes = ?,
workplace_id = ?,
map_x_percent = ?,
map_y_percent = ?,
map_width_percent = ?,
map_height_percent = ?,
updated_at = NOW()
WHERE equipment_id = ?`,
values
);
if (result.affectedRows === 0) {
throw new Error('Equipment not found');
}
return { equipment_id: equipmentId, ...equipmentData };
},
// UPDATE MAP POSITION - 지도상 위치 업데이트
updateMapPosition: async (equipmentId, positionData) => {
const db = await getDb();
const hasWorkplaceId = positionData.workplace_id !== undefined;
const query = hasWorkplaceId ? `
UPDATE equipments SET
workplace_id = ?,
map_x_percent = ?,
map_y_percent = ?,
map_width_percent = ?,
map_height_percent = ?,
updated_at = NOW()
WHERE equipment_id = ?
` : `
UPDATE equipments SET
map_x_percent = ?,
map_y_percent = ?,
map_width_percent = ?,
map_height_percent = ?,
updated_at = NOW()
WHERE equipment_id = ?
`;
const values = hasWorkplaceId ? [
positionData.workplace_id,
positionData.map_x_percent,
positionData.map_y_percent,
positionData.map_width_percent,
positionData.map_height_percent,
equipmentId
] : [
positionData.map_x_percent,
positionData.map_y_percent,
positionData.map_width_percent,
positionData.map_height_percent,
equipmentId
];
const [result] = await db.query(query, values);
if (result.affectedRows === 0) {
throw new Error('Equipment not found');
}
return { equipment_id: equipmentId, ...positionData };
},
// DELETE - 설비 삭제
delete: async (equipmentId) => {
const db = await getDb();
const [result] = await db.query('DELETE FROM equipments WHERE equipment_id = ?', [equipmentId]);
if (result.affectedRows === 0) {
throw new Error('Equipment not found');
}
return { equipment_id: equipmentId };
},
// CHECK DUPLICATE CODE - 설비 코드 중복 확인
checkDuplicateCode: async (equipmentCode, excludeEquipmentId) => {
const db = await getDb();
let query = 'SELECT equipment_id FROM equipments WHERE equipment_code = ?';
const values = [equipmentCode];
if (excludeEquipmentId) {
query += ' AND equipment_id != ?';
values.push(excludeEquipmentId);
}
const [rows] = await db.query(query, values);
return rows.length > 0;
},
// GET EQUIPMENT TYPES - 사용 중인 설비 유형 목록 조회
getEquipmentTypes: async () => {
const db = await getDb();
const [rows] = await db.query(
`SELECT DISTINCT equipment_type
FROM equipments
WHERE equipment_type IS NOT NULL
ORDER BY equipment_type ASC`
);
return rows.map(row => row.equipment_type);
},
// GET NEXT EQUIPMENT CODE - 다음 관리번호 자동 생성
getNextEquipmentCode: async (prefix = 'TKP') => {
const db = await getDb();
const [rows] = await db.query(
`SELECT equipment_code
FROM equipments
WHERE equipment_code LIKE ?
ORDER BY equipment_code DESC
LIMIT 1`,
[`${prefix}-%`]
);
let nextNumber = 1;
if (rows.length > 0) {
const lastCode = rows[0].equipment_code;
const match = lastCode.match(new RegExp(`^${prefix}-(\\d+)$`));
if (match) {
nextNumber = parseInt(match[1], 10) + 1;
}
}
return `${prefix}-${String(nextNumber).padStart(3, '0')}`;
},
// ==========================================
// 설비 사진 관리
// ==========================================
addPhoto: async (equipmentId, photoData) => {
const db = await getDb();
const values = [
equipmentId,
photoData.photo_path,
photoData.description || null,
photoData.display_order || 0,
photoData.uploaded_by || null
];
const [result] = await db.query(
`INSERT INTO equipment_photos (
equipment_id, photo_path, description, display_order, uploaded_by
) VALUES (?, ?, ?, ?, ?)`,
values
);
return {
photo_id: result.insertId,
equipment_id: equipmentId,
...photoData
};
},
getPhotos: async (equipmentId) => {
const db = await getDb();
const [rows] = await db.query(
`SELECT ep.*, u.name AS uploaded_by_name
FROM equipment_photos ep
LEFT JOIN users u ON ep.uploaded_by = u.user_id
WHERE ep.equipment_id = ?
ORDER BY ep.display_order ASC, ep.created_at ASC`,
[equipmentId]
);
return rows;
},
deletePhoto: async (photoId) => {
const db = await getDb();
const [photo] = await db.query(
'SELECT photo_path FROM equipment_photos WHERE photo_id = ?',
[photoId]
);
const [result] = await db.query('DELETE FROM equipment_photos WHERE photo_id = ?', [photoId]);
if (result.affectedRows === 0) {
throw new Error('Photo not found');
}
return { photo_id: photoId, photo_path: photo[0]?.photo_path };
},
// ==========================================
// 설비 임시 이동
// ==========================================
moveTemporarily: async (equipmentId, moveData) => {
const db = await getDb();
const [result] = await db.query(
`UPDATE equipments SET
current_workplace_id = ?,
current_map_x_percent = ?,
current_map_y_percent = ?,
current_map_width_percent = ?,
current_map_height_percent = ?,
is_temporarily_moved = TRUE,
moved_at = NOW(),
moved_by = ?,
updated_at = NOW()
WHERE equipment_id = ?`,
[
moveData.target_workplace_id,
moveData.target_x_percent,
moveData.target_y_percent,
moveData.target_width_percent || null,
moveData.target_height_percent || null,
moveData.moved_by || null,
equipmentId
]
);
if (result.affectedRows === 0) {
throw new Error('Equipment not found');
}
await db.query(
`INSERT INTO equipment_move_logs (
equipment_id, move_type,
from_workplace_id, to_workplace_id,
from_x_percent, from_y_percent,
to_x_percent, to_y_percent,
reason, moved_by
) VALUES (?, 'temporary', ?, ?, ?, ?, ?, ?, ?, ?)`,
[
equipmentId,
moveData.from_workplace_id || null,
moveData.target_workplace_id,
moveData.from_x_percent || null,
moveData.from_y_percent || null,
moveData.target_x_percent,
moveData.target_y_percent,
moveData.reason || null,
moveData.moved_by || null
]
);
return { equipment_id: equipmentId, moved: true };
},
returnToOriginal: async (equipmentId, userId) => {
const db = await getDb();
const [equipment] = await db.query(
'SELECT current_workplace_id, current_map_x_percent, current_map_y_percent FROM equipments WHERE equipment_id = ?',
[equipmentId]
);
if (!equipment[0]) {
throw new Error('Equipment not found');
}
await db.query(
`UPDATE equipments SET
current_workplace_id = NULL,
current_map_x_percent = NULL,
current_map_y_percent = NULL,
current_map_width_percent = NULL,
current_map_height_percent = NULL,
is_temporarily_moved = FALSE,
moved_at = NULL,
moved_by = NULL,
updated_at = NOW()
WHERE equipment_id = ?`,
[equipmentId]
);
await db.query(
`INSERT INTO equipment_move_logs (
equipment_id, move_type,
from_workplace_id, from_x_percent, from_y_percent,
reason, moved_by
) VALUES (?, 'return', ?, ?, ?, '원위치 복귀', ?)`,
[
equipmentId,
equipment[0].current_workplace_id,
equipment[0].current_map_x_percent,
equipment[0].current_map_y_percent,
userId || null
]
);
return { equipment_id: equipmentId, returned: true };
},
getTemporarilyMoved: async () => {
const db = await getDb();
const [rows] = await db.query(
`SELECT
e.*,
w_orig.workplace_name AS original_workplace_name,
w_curr.workplace_name AS current_workplace_name,
u.name AS moved_by_name
FROM equipments e
LEFT JOIN workplaces w_orig ON e.workplace_id = w_orig.workplace_id
LEFT JOIN workplaces w_curr ON e.current_workplace_id = w_curr.workplace_id
LEFT JOIN users u ON e.moved_by = u.user_id
WHERE e.is_temporarily_moved = TRUE
ORDER BY e.moved_at DESC`
);
return rows;
},
getMoveLogs: async (equipmentId) => {
const db = await getDb();
const [rows] = await db.query(
`SELECT
eml.*,
w_from.workplace_name AS from_workplace_name,
w_to.workplace_name AS to_workplace_name,
u.name AS moved_by_name
FROM equipment_move_logs eml
LEFT JOIN workplaces w_from ON eml.from_workplace_id = w_from.workplace_id
LEFT JOIN workplaces w_to ON eml.to_workplace_id = w_to.workplace_id
LEFT JOIN users u ON eml.moved_by = u.user_id
WHERE eml.equipment_id = ?
ORDER BY eml.moved_at DESC`,
[equipmentId]
);
return rows;
},
// ==========================================
// 설비 외부 반출/반입
// ==========================================
exportEquipment: async (exportData) => {
const db = await getDb();
const [logResult] = await db.query(
`INSERT INTO equipment_external_logs (
equipment_id, log_type, export_date, expected_return_date,
destination, reason, notes, exported_by
) VALUES (?, 'export', ?, ?, ?, ?, ?, ?)`,
[
exportData.equipment_id,
exportData.export_date || new Date().toISOString().slice(0, 10),
exportData.expected_return_date || null,
exportData.destination || null,
exportData.reason || null,
exportData.notes || null,
exportData.exported_by || null
]
);
const status = exportData.is_repair ? 'repair_external' : 'external';
await db.query(
'UPDATE equipments SET status = ?, updated_at = NOW() WHERE equipment_id = ?',
[status, exportData.equipment_id]
);
return {
log_id: logResult.insertId,
equipment_id: exportData.equipment_id,
exported: true
};
},
returnEquipment: async (logId, returnData) => {
const db = await getDb();
const [logs] = await db.query(
'SELECT equipment_id FROM equipment_external_logs WHERE log_id = ?',
[logId]
);
if (!logs[0]) {
throw new Error('Export log not found');
}
const equipmentId = logs[0].equipment_id;
await db.query(
`UPDATE equipment_external_logs SET
actual_return_date = ?,
returned_by = ?,
notes = CONCAT(IFNULL(notes, ''), '\n반입: ', IFNULL(?, '')),
updated_at = NOW()
WHERE log_id = ?`,
[
returnData.return_date || new Date().toISOString().slice(0, 10),
returnData.returned_by || null,
returnData.notes || '',
logId
]
);
await db.query(
'UPDATE equipments SET status = ?, updated_at = NOW() WHERE equipment_id = ?',
[returnData.new_status || 'active', equipmentId]
);
return {
log_id: logId,
equipment_id: equipmentId,
returned: true
};
},
getExternalLogs: async (equipmentId) => {
const db = await getDb();
const [rows] = await db.query(
`SELECT
eel.*,
u_exp.name AS exported_by_name,
u_ret.name AS returned_by_name
FROM equipment_external_logs eel
LEFT JOIN users u_exp ON eel.exported_by = u_exp.user_id
LEFT JOIN users u_ret ON eel.returned_by = u_ret.user_id
WHERE eel.equipment_id = ?
ORDER BY eel.created_at DESC`,
[equipmentId]
);
return rows;
},
getExportedEquipments: async () => {
const db = await getDb();
const [rows] = await db.query(
`SELECT
e.*,
w.workplace_name,
eel.export_date,
eel.expected_return_date,
eel.destination,
eel.reason,
u.name AS exported_by_name
FROM equipments e
INNER JOIN (
SELECT equipment_id, MAX(log_id) AS latest_log_id
FROM equipment_external_logs
WHERE actual_return_date IS NULL
GROUP BY equipment_id
) latest ON e.equipment_id = latest.equipment_id
INNER JOIN equipment_external_logs eel ON eel.log_id = latest.latest_log_id
LEFT JOIN workplaces w ON e.workplace_id = w.workplace_id
LEFT JOIN users u ON eel.exported_by = u.user_id
WHERE e.status IN ('external', 'repair_external')
ORDER BY eel.export_date DESC`
);
return rows;
},
// ==========================================
// 설비 수리 신청
// ==========================================
createRepairRequest: async (requestData) => {
const db = await getDb();
const [categories] = await db.query(
"SELECT category_id FROM issue_report_categories WHERE category_name = '설비 수리' LIMIT 1"
);
if (!categories[0]) {
throw new Error('설비 수리 카테고리가 없습니다');
}
const categoryId = categories[0].category_id;
let itemId = requestData.item_id;
if (!itemId) {
const [items] = await db.query(
'SELECT item_id FROM issue_report_items WHERE category_id = ? LIMIT 1',
[categoryId]
);
itemId = items[0]?.item_id;
}
const photos = requestData.photo_paths || [];
const [result] = await db.query(
`INSERT INTO work_issue_reports (
reporter_id, issue_category_id, issue_item_id,
workplace_id, equipment_id,
additional_description,
photo_path1, photo_path2, photo_path3, photo_path4, photo_path5,
status
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'reported')`,
[
requestData.reported_by || null,
categoryId,
itemId,
requestData.workplace_id || null,
requestData.equipment_id,
requestData.description || null,
photos[0] || null,
photos[1] || null,
photos[2] || null,
photos[3] || null,
photos[4] || null
]
);
await db.query(
'UPDATE equipments SET status = ?, updated_at = NOW() WHERE equipment_id = ?',
['repair_needed', requestData.equipment_id]
);
// fire-and-forget: 알림 실패가 수리 신청을 블로킹하면 안 됨
notifyHelper.send({
type: 'repair',
title: `수리 신청: ${requestData.equipment_name || '설비'}`,
message: `${requestData.repair_type || '일반 수리'} 수리가 신청되었습니다.`,
link_url: '/pages/admin/repair-management.html',
reference_type: 'work_issue_reports',
reference_id: result.insertId,
created_by: requestData.reported_by
}).catch(() => {});
return {
report_id: result.insertId,
equipment_id: requestData.equipment_id,
created: true
};
},
getRepairHistory: async (equipmentId) => {
const db = await getDb();
const [rows] = await db.query(
`SELECT
wir.*,
irc.category_name,
iri.item_name,
u_rep.name AS reported_by_name,
u_res.name AS resolved_by_name,
w.workplace_name
FROM work_issue_reports wir
LEFT 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 u_rep ON wir.reporter_id = u_rep.user_id
LEFT JOIN users u_res ON wir.resolved_by = u_res.user_id
LEFT JOIN workplaces w ON wir.workplace_id = w.workplace_id
WHERE wir.equipment_id = ?
ORDER BY wir.created_at DESC`,
[equipmentId]
);
return rows;
},
getRepairCategories: async () => {
const db = await getDb();
const [rows] = await db.query(
`SELECT iri.item_id, iri.item_name, iri.description, iri.severity
FROM issue_report_items iri
INNER JOIN issue_report_categories irc ON iri.category_id = irc.category_id
WHERE irc.category_name = '설비 수리' AND iri.is_active = 1
ORDER BY iri.display_order ASC`
);
return rows;
},
addRepairCategory: async (itemName) => {
const db = await getDb();
const [categories] = await db.query(
"SELECT category_id FROM issue_report_categories WHERE category_name = '설비 수리'"
);
if (categories.length === 0) {
throw new Error('설비 수리 카테고리가 없습니다.');
}
const categoryId = categories[0].category_id;
const [existing] = await db.query(
'SELECT item_id FROM issue_report_items WHERE category_id = ? AND item_name = ?',
[categoryId, itemName]
);
if (existing.length > 0) {
return { item_id: existing[0].item_id, item_name: itemName, isNew: false };
}
const [maxOrder] = await db.query(
'SELECT MAX(display_order) as max_order FROM issue_report_items WHERE category_id = ?',
[categoryId]
);
const nextOrder = (maxOrder[0].max_order || 0) + 1;
const [result] = await db.query(
`INSERT INTO issue_report_items (category_id, item_name, display_order, is_active)
VALUES (?, ?, ?, 1)`,
[categoryId, itemName, nextOrder]
);
return { item_id: result.insertId, item_name: itemName, isNew: true };
}
};
module.exports = EquipmentModel;