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:
Hyungi Ahn
2026-04-01 10:21:16 +09:00
parent 41bb755181
commit 5832755475
7 changed files with 323 additions and 208 deletions

View File

@@ -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 {