fix(purchase): 모바일 네비 수정 + 권한 자동허용 + 장바구니 다중 품목 신청
- sideNav: TBM 패턴(hidden lg:flex + mobile-open) 적용, 회색 오버레이 버그 수정 - 권한: NAV_MENU 기반 publicPageKeys 추출, admin이 아닌 페이지는 비관리자 자동 접근 허용 - 장바구니: 검색→품목 추가→계속 검색→추가, 동일 품목 수량 합산, 품목별 메모 - bulk API: POST /purchase-requests/bulk (트랜잭션, is_new 품목 마스터 등록 포함) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -180,6 +180,87 @@ const PurchaseRequestController = {
|
||||
}
|
||||
},
|
||||
|
||||
// 일괄 신청 (장바구니, 트랜잭션)
|
||||
bulkCreate: async (req, res) => {
|
||||
const { getDb } = require('../dbPool');
|
||||
let conn;
|
||||
try {
|
||||
const { items, photo } = req.body;
|
||||
if (!items || !Array.isArray(items) || items.length === 0) {
|
||||
return res.status(400).json({ success: false, message: '신청할 품목을 선택해주세요.' });
|
||||
}
|
||||
for (const item of items) {
|
||||
if (!item.item_id && !item.item_name) {
|
||||
return res.status(400).json({ success: false, message: '품목 정보가 올바르지 않습니다.' });
|
||||
}
|
||||
if (!item.quantity || item.quantity < 1) {
|
||||
return res.status(400).json({ success: false, message: '수량은 1 이상이어야 합니다.' });
|
||||
}
|
||||
}
|
||||
|
||||
// 사진 업로드 (트랜잭션 밖 — 파일은 롤백 불가)
|
||||
let photo_path = null;
|
||||
if (photo) {
|
||||
photo_path = await saveBase64Image(photo, 'pr', 'purchase_requests');
|
||||
}
|
||||
|
||||
const db = await getDb();
|
||||
conn = await db.getConnection();
|
||||
await conn.beginTransaction();
|
||||
|
||||
const createdIds = [];
|
||||
let newItemRegistered = false;
|
||||
|
||||
for (const item of items) {
|
||||
let itemId = item.item_id || null;
|
||||
|
||||
// 신규 품목 등록 (is_new)
|
||||
if (item.is_new && item.item_name) {
|
||||
const [existing] = await conn.query(
|
||||
`SELECT item_id FROM consumable_items
|
||||
WHERE item_name = ? AND (spec = ? OR (spec IS NULL AND ? IS NULL))
|
||||
AND (maker = ? OR (maker IS NULL AND ? IS NULL))`,
|
||||
[item.item_name.trim(), item.spec || null, item.spec || null, item.maker || null, item.maker || null]
|
||||
);
|
||||
if (existing.length > 0) {
|
||||
itemId = existing[0].item_id;
|
||||
} else {
|
||||
const [ins] = await conn.query(
|
||||
`INSERT INTO consumable_items (item_name, spec, maker, category, is_active) VALUES (?, ?, ?, ?, 1)`,
|
||||
[item.item_name.trim(), item.spec || null, item.maker || null, item.category || 'consumable']
|
||||
);
|
||||
itemId = ins.insertId;
|
||||
newItemRegistered = true;
|
||||
}
|
||||
}
|
||||
|
||||
// purchase_request 생성
|
||||
const [result] = await conn.query(
|
||||
`INSERT INTO purchase_requests (item_id, custom_item_name, custom_category, quantity, requester_id, request_date, notes, photo_path)
|
||||
VALUES (?, ?, ?, ?, ?, CURDATE(), ?, ?)`,
|
||||
[itemId, item.is_new && !itemId ? item.item_name : null, item.is_new && !itemId ? item.category : null,
|
||||
item.quantity, req.user.id, item.notes || null, photo_path]
|
||||
);
|
||||
createdIds.push(result.insertId);
|
||||
}
|
||||
|
||||
await conn.commit();
|
||||
if (newItemRegistered) koreanSearch.clearCache();
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: { request_ids: createdIds, count: createdIds.length },
|
||||
message: `${createdIds.length}건의 소모품 신청이 등록되었습니다.`
|
||||
});
|
||||
} catch (err) {
|
||||
if (conn) await conn.rollback().catch(() => {});
|
||||
logger.error('bulkCreate error:', err);
|
||||
res.status(500).json({ success: false, message: '서버 오류가 발생했습니다.' });
|
||||
} finally {
|
||||
if (conn) conn.release();
|
||||
}
|
||||
},
|
||||
|
||||
// 스마트 검색 (초성 + 별칭 + substring)
|
||||
search: async (req, res) => {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user