feat(purchase): 생산소모품 구매 관리 시스템 구현
tkuser: 업체(공급업체) CRUD + 소모품 마스터 CRUD (사진 업로드 포함) tkfb: 구매신청 → 구매 처리 → 월간 분석/정산 전체 워크플로 설비(equipment) 분류 구매 시 자동 등록 + 실패 시 admin 알림 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
104
system1-factory/api/controllers/purchaseController.js
Normal file
104
system1-factory/api/controllers/purchaseController.js
Normal file
@@ -0,0 +1,104 @@
|
||||
const PurchaseModel = require('../models/purchaseModel');
|
||||
const PurchaseRequestModel = require('../models/purchaseRequestModel');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
const PurchaseController = {
|
||||
// 구매 처리 (신청 → 구매)
|
||||
create: async (req, res) => {
|
||||
try {
|
||||
const { request_id, item_id, vendor_id, quantity, unit_price, purchase_date, update_base_price, notes } = req.body;
|
||||
|
||||
if (!item_id) return res.status(400).json({ success: false, message: '소모품을 선택해주세요.' });
|
||||
if (!unit_price) return res.status(400).json({ success: false, message: '구매 단가를 입력해주세요.' });
|
||||
if (!purchase_date) return res.status(400).json({ success: false, message: '구매일을 입력해주세요.' });
|
||||
|
||||
// 구매 내역 생성
|
||||
const purchaseId = await PurchaseModel.createFromRequest({
|
||||
request_id: request_id || null,
|
||||
item_id,
|
||||
vendor_id: vendor_id || null,
|
||||
quantity: quantity || 1,
|
||||
unit_price,
|
||||
purchase_date,
|
||||
purchaser_id: req.user.id,
|
||||
notes
|
||||
});
|
||||
|
||||
// 기준가 업데이트 요청 시
|
||||
if (update_base_price) {
|
||||
const items = await PurchaseModel.getConsumableItems(false);
|
||||
const item = items.find(i => i.item_id === parseInt(item_id));
|
||||
if (item) {
|
||||
await PurchaseModel.updateBasePrice(item_id, unit_price, item.base_price, req.user.id);
|
||||
}
|
||||
}
|
||||
|
||||
// 설비 자동 등록 (category='equipment')
|
||||
let equipmentResult = null;
|
||||
if (request_id) {
|
||||
const requestData = await PurchaseRequestModel.getById(request_id);
|
||||
if (requestData && requestData.category === 'equipment') {
|
||||
equipmentResult = await PurchaseModel.tryAutoRegisterEquipment({
|
||||
item_name: requestData.item_name,
|
||||
maker: requestData.maker,
|
||||
vendor_name: null,
|
||||
unit_price,
|
||||
purchase_date,
|
||||
purchase_id: purchaseId,
|
||||
purchaser_id: req.user.id
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// 직접 구매 시에도 category 확인
|
||||
const items = await PurchaseModel.getConsumableItems(false);
|
||||
const item = items.find(i => i.item_id === parseInt(item_id));
|
||||
if (item && item.category === 'equipment') {
|
||||
const vendors = await PurchaseModel.getVendors();
|
||||
const vendor = vendors.find(v => v.vendor_id === parseInt(vendor_id));
|
||||
equipmentResult = await PurchaseModel.tryAutoRegisterEquipment({
|
||||
item_name: item.item_name,
|
||||
maker: item.maker,
|
||||
vendor_name: vendor ? vendor.vendor_name : null,
|
||||
unit_price,
|
||||
purchase_date,
|
||||
purchase_id: purchaseId,
|
||||
purchaser_id: req.user.id
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const result = { purchase_id: purchaseId };
|
||||
if (equipmentResult) result.equipment = equipmentResult;
|
||||
|
||||
res.status(201).json({ success: true, data: result, message: '구매 처리가 완료되었습니다.' });
|
||||
} catch (err) {
|
||||
logger.error('Purchase create error:', err);
|
||||
res.status(500).json({ success: false, message: '서버 오류가 발생했습니다.' });
|
||||
}
|
||||
},
|
||||
|
||||
// 구매 내역 목록
|
||||
getAll: async (req, res) => {
|
||||
try {
|
||||
const { vendor_id, category, from_date, to_date, year_month } = req.query;
|
||||
const rows = await PurchaseModel.getAll({ vendor_id, category, from_date, to_date, year_month });
|
||||
res.json({ success: true, data: rows });
|
||||
} catch (err) {
|
||||
logger.error('Purchase getAll error:', err);
|
||||
res.status(500).json({ success: false, message: '서버 오류가 발생했습니다.' });
|
||||
}
|
||||
},
|
||||
|
||||
// 가격 변동 이력
|
||||
getPriceHistory: async (req, res) => {
|
||||
try {
|
||||
const rows = await PurchaseModel.getPriceHistory(req.params.itemId);
|
||||
res.json({ success: true, data: rows });
|
||||
} catch (err) {
|
||||
logger.error('PriceHistory get error:', err);
|
||||
res.status(500).json({ success: false, message: '서버 오류가 발생했습니다.' });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = PurchaseController;
|
||||
120
system1-factory/api/controllers/purchaseRequestController.js
Normal file
120
system1-factory/api/controllers/purchaseRequestController.js
Normal file
@@ -0,0 +1,120 @@
|
||||
const PurchaseRequestModel = require('../models/purchaseRequestModel');
|
||||
const PurchaseModel = require('../models/purchaseModel');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
const PurchaseRequestController = {
|
||||
// 구매신청 목록
|
||||
getAll: async (req, res) => {
|
||||
try {
|
||||
const { status, category, from_date, to_date } = req.query;
|
||||
const isAdmin = req.user && ['admin', 'system'].includes(req.user.access_level);
|
||||
const filters = { status, category, from_date, to_date };
|
||||
if (!isAdmin) filters.requester_id = req.user.id;
|
||||
const rows = await PurchaseRequestModel.getAll(filters);
|
||||
res.json({ success: true, data: rows });
|
||||
} catch (err) {
|
||||
logger.error('PurchaseRequest getAll error:', err);
|
||||
res.status(500).json({ success: false, message: '서버 오류가 발생했습니다.' });
|
||||
}
|
||||
},
|
||||
|
||||
// 구매신청 상세
|
||||
getById: async (req, res) => {
|
||||
try {
|
||||
const row = await PurchaseRequestModel.getById(req.params.id);
|
||||
if (!row) return res.status(404).json({ success: false, message: '신청 건을 찾을 수 없습니다.' });
|
||||
res.json({ success: true, data: row });
|
||||
} catch (err) {
|
||||
logger.error('PurchaseRequest getById error:', err);
|
||||
res.status(500).json({ success: false, message: '서버 오류가 발생했습니다.' });
|
||||
}
|
||||
},
|
||||
|
||||
// 구매신청 생성
|
||||
create: async (req, res) => {
|
||||
try {
|
||||
const { item_id, quantity, notes } = req.body;
|
||||
if (!item_id) return res.status(400).json({ success: false, message: '소모품을 선택해주세요.' });
|
||||
if (!quantity || quantity < 1) return res.status(400).json({ success: false, message: '수량은 1 이상이어야 합니다.' });
|
||||
|
||||
const request = await PurchaseRequestModel.create({
|
||||
item_id,
|
||||
quantity,
|
||||
requester_id: req.user.id,
|
||||
request_date: new Date().toISOString().substring(0, 10),
|
||||
notes
|
||||
});
|
||||
res.status(201).json({ success: true, data: request, message: '구매신청이 등록되었습니다.' });
|
||||
} catch (err) {
|
||||
logger.error('PurchaseRequest create error:', err);
|
||||
res.status(500).json({ success: false, message: '서버 오류가 발생했습니다.' });
|
||||
}
|
||||
},
|
||||
|
||||
// 보류 처리 (admin)
|
||||
hold: async (req, res) => {
|
||||
try {
|
||||
const { hold_reason } = req.body;
|
||||
const request = await PurchaseRequestModel.hold(req.params.id, hold_reason);
|
||||
if (!request) return res.status(404).json({ success: false, message: '신청 건을 찾을 수 없습니다.' });
|
||||
res.json({ success: true, data: request, message: '보류 처리되었습니다.' });
|
||||
} catch (err) {
|
||||
logger.error('PurchaseRequest hold error:', err);
|
||||
res.status(500).json({ success: false, message: '서버 오류가 발생했습니다.' });
|
||||
}
|
||||
},
|
||||
|
||||
// pending으로 되돌리기 (admin)
|
||||
revert: async (req, res) => {
|
||||
try {
|
||||
const request = await PurchaseRequestModel.revertToPending(req.params.id);
|
||||
if (!request) return res.status(404).json({ success: false, message: '신청 건을 찾을 수 없습니다.' });
|
||||
res.json({ success: true, data: request, message: '대기 상태로 되돌렸습니다.' });
|
||||
} catch (err) {
|
||||
logger.error('PurchaseRequest revert error:', err);
|
||||
res.status(500).json({ success: false, message: '서버 오류가 발생했습니다.' });
|
||||
}
|
||||
},
|
||||
|
||||
// 삭제 (본인 + pending만)
|
||||
delete: async (req, res) => {
|
||||
try {
|
||||
const existing = await PurchaseRequestModel.getById(req.params.id);
|
||||
if (!existing) return res.status(404).json({ success: false, message: '신청 건을 찾을 수 없습니다.' });
|
||||
const isAdmin = req.user && ['admin', 'system'].includes(req.user.access_level);
|
||||
if (!isAdmin && existing.requester_id !== req.user.id) {
|
||||
return res.status(403).json({ success: false, message: '본인의 신청만 삭제할 수 있습니다.' });
|
||||
}
|
||||
const deleted = await PurchaseRequestModel.delete(req.params.id);
|
||||
if (!deleted) return res.status(400).json({ success: false, message: '대기 상태의 신청만 삭제할 수 있습니다.' });
|
||||
res.json({ success: true, message: '삭제되었습니다.' });
|
||||
} catch (err) {
|
||||
logger.error('PurchaseRequest delete error:', err);
|
||||
res.status(500).json({ success: false, message: '서버 오류가 발생했습니다.' });
|
||||
}
|
||||
},
|
||||
|
||||
// 소모품 목록 (select용)
|
||||
getConsumableItems: async (req, res) => {
|
||||
try {
|
||||
const items = await PurchaseModel.getConsumableItems();
|
||||
res.json({ success: true, data: items });
|
||||
} catch (err) {
|
||||
logger.error('ConsumableItems get error:', err);
|
||||
res.status(500).json({ success: false, message: '서버 오류가 발생했습니다.' });
|
||||
}
|
||||
},
|
||||
|
||||
// 업체 목록 (select용)
|
||||
getVendors: async (req, res) => {
|
||||
try {
|
||||
const vendors = await PurchaseModel.getVendors();
|
||||
res.json({ success: true, data: vendors });
|
||||
} catch (err) {
|
||||
logger.error('Vendors get error:', err);
|
||||
res.status(500).json({ success: false, message: '서버 오류가 발생했습니다.' });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = PurchaseRequestController;
|
||||
76
system1-factory/api/controllers/settlementController.js
Normal file
76
system1-factory/api/controllers/settlementController.js
Normal file
@@ -0,0 +1,76 @@
|
||||
const SettlementModel = require('../models/settlementModel');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
const SettlementController = {
|
||||
// 월간 요약 (분류별 + 업체별)
|
||||
getMonthlySummary: async (req, res) => {
|
||||
try {
|
||||
const { year_month } = req.query;
|
||||
if (!year_month) return res.status(400).json({ success: false, message: '년월을 선택해주세요.' });
|
||||
|
||||
const [categorySummary, vendorSummary] = await Promise.all([
|
||||
SettlementModel.getCategorySummary(year_month),
|
||||
SettlementModel.getVendorSummary(year_month)
|
||||
]);
|
||||
|
||||
res.json({ success: true, data: { categorySummary, vendorSummary } });
|
||||
} catch (err) {
|
||||
logger.error('Settlement summary error:', err);
|
||||
res.status(500).json({ success: false, message: '서버 오류가 발생했습니다.' });
|
||||
}
|
||||
},
|
||||
|
||||
// 월간 상세 구매 목록
|
||||
getMonthlyPurchases: async (req, res) => {
|
||||
try {
|
||||
const { year_month } = req.query;
|
||||
if (!year_month) return res.status(400).json({ success: false, message: '년월을 선택해주세요.' });
|
||||
const rows = await SettlementModel.getMonthlyPurchases(year_month);
|
||||
res.json({ success: true, data: rows });
|
||||
} catch (err) {
|
||||
logger.error('Settlement purchases error:', err);
|
||||
res.status(500).json({ success: false, message: '서버 오류가 발생했습니다.' });
|
||||
}
|
||||
},
|
||||
|
||||
// 가격 변동 목록
|
||||
getPriceChanges: async (req, res) => {
|
||||
try {
|
||||
const { year_month } = req.query;
|
||||
if (!year_month) return res.status(400).json({ success: false, message: '년월을 선택해주세요.' });
|
||||
const rows = await SettlementModel.getPriceChanges(year_month);
|
||||
res.json({ success: true, data: rows });
|
||||
} catch (err) {
|
||||
logger.error('Settlement priceChanges error:', err);
|
||||
res.status(500).json({ success: false, message: '서버 오류가 발생했습니다.' });
|
||||
}
|
||||
},
|
||||
|
||||
// 정산 완료
|
||||
complete: async (req, res) => {
|
||||
try {
|
||||
const { year_month, vendor_id, notes } = req.body;
|
||||
if (!year_month || !vendor_id) return res.status(400).json({ success: false, message: '년월과 업체를 선택해주세요.' });
|
||||
const result = await SettlementModel.completeSettlement(year_month, vendor_id, req.user.id, notes);
|
||||
res.json({ success: true, data: result, message: '정산 완료 처리되었습니다.' });
|
||||
} catch (err) {
|
||||
logger.error('Settlement complete error:', err);
|
||||
res.status(500).json({ success: false, message: '서버 오류가 발생했습니다.' });
|
||||
}
|
||||
},
|
||||
|
||||
// 정산 취소
|
||||
cancel: async (req, res) => {
|
||||
try {
|
||||
const { year_month, vendor_id } = req.body;
|
||||
if (!year_month || !vendor_id) return res.status(400).json({ success: false, message: '년월과 업체를 선택해주세요.' });
|
||||
const result = await SettlementModel.cancelSettlement(year_month, vendor_id);
|
||||
res.json({ success: true, data: result, message: '정산이 취소되었습니다.' });
|
||||
} catch (err) {
|
||||
logger.error('Settlement cancel error:', err);
|
||||
res.status(500).json({ success: false, message: '서버 오류가 발생했습니다.' });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = SettlementController;
|
||||
Reference in New Issue
Block a user