- 소모품 select → 검색형 드롭다운 (debounce + 키보드 탐색) - 미등록 품목 직접 입력 + 분류 선택 지원 - 사진 첨부 (base64 업로드, HEIC→JPEG 프론트 변환) - 구매 처리 시 미등록 품목 소모품 마스터 자동 등록 - item_id NULL 허용, LEFT JOIN, custom_item_name/custom_category/photo_path 컬럼 - DB 마이그레이션 필요: ALTER TABLE purchase_requests Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
140 lines
5.8 KiB
JavaScript
140 lines
5.8 KiB
JavaScript
const PurchaseRequestModel = require('../models/purchaseRequestModel');
|
|
const PurchaseModel = require('../models/purchaseModel');
|
|
const { saveBase64Image } = require('../services/imageUploadService');
|
|
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, custom_item_name, custom_category, quantity, notes, photo } = req.body;
|
|
|
|
// item_id 또는 custom_item_name 중 하나 필수
|
|
if (!item_id && !custom_item_name) {
|
|
return res.status(400).json({ success: false, message: '소모품을 선택하거나 품목명을 입력해주세요.' });
|
|
}
|
|
if (!quantity || quantity < 1) {
|
|
return res.status(400).json({ success: false, message: '수량은 1 이상이어야 합니다.' });
|
|
}
|
|
if (!item_id && custom_item_name && !custom_category) {
|
|
return res.status(400).json({ success: false, message: '직접 입력 시 분류를 선택해주세요.' });
|
|
}
|
|
|
|
// 사진 업로드
|
|
let photo_path = null;
|
|
if (photo) {
|
|
photo_path = await saveBase64Image(photo, 'pr', 'purchase_requests');
|
|
}
|
|
|
|
const request = await PurchaseRequestModel.create({
|
|
item_id: item_id || null,
|
|
custom_item_name: custom_item_name || null,
|
|
custom_category: custom_category || null,
|
|
quantity,
|
|
requester_id: req.user.id,
|
|
request_date: new Date().toISOString().substring(0, 10),
|
|
notes,
|
|
photo_path
|
|
});
|
|
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;
|