diff --git a/backend-node/src/controllers/outboundController.ts b/backend-node/src/controllers/outboundController.ts index 86156ca5..b4b942a0 100644 --- a/backend-node/src/controllers/outboundController.ts +++ b/backend-node/src/controllers/outboundController.ts @@ -221,10 +221,10 @@ export async function create(req: AuthenticatedRequest, res: Response) { const afterQty = afterStockRes.rows[0]?.current_qty || '0'; await client.query( `INSERT INTO inventory_history ( - id, company_code, item_number, warehouse_code, location_code, - history_type, history_date, change_qty, after_qty, reason, - created_by, created_date - ) VALUES (gen_random_uuid()::text, $1, $2, $3, $4, '출고', NOW()::date, $5, $6, $7, $8, NOW())`, + id, company_code, item_code, warehouse_code, location_code, + transaction_type, transaction_date, quantity, balance_qty, remark, + writer, created_date + ) VALUES (gen_random_uuid()::text, $1, $2, $3, $4, '출고', NOW(), $5, $6, $7, $8, NOW())`, [companyCode, itemCode, whCode, locCode, String(-outQty), afterQty, item.outbound_type || '출고', userId] ); } @@ -515,7 +515,7 @@ export async function getWarehouses(req: AuthenticatedRequest, res: Response) { const result = await pool.query( `SELECT warehouse_code, warehouse_name, warehouse_type FROM warehouse_info - WHERE company_code = $1 AND status != '삭제' + WHERE company_code = $1 AND COALESCE(status, '') != '삭제' ORDER BY warehouse_name`, [companyCode] ); @@ -526,3 +526,25 @@ export async function getWarehouses(req: AuthenticatedRequest, res: Response) { return res.status(500).json({ success: false, message: error.message }); } } + +// 창고별 위치 목록 조회 +export async function getLocations(req: AuthenticatedRequest, res: Response) { + try { + const companyCode = req.user!.companyCode; + const warehouseCode = req.query.warehouse_code as string; + const pool = getPool(); + + const result = await pool.query( + `SELECT location_code, location_name, warehouse_code + FROM warehouse_location + WHERE company_code = $1 ${warehouseCode ? "AND warehouse_code = $2" : ""} + ORDER BY location_code`, + warehouseCode ? [companyCode, warehouseCode] : [companyCode] + ); + + return res.json({ success: true, data: result.rows }); + } catch (error: any) { + logger.error("위치 목록 조회 실패", { error: error.message }); + return res.status(500).json({ success: false, message: error.message }); + } +} diff --git a/backend-node/src/controllers/receivingController.ts b/backend-node/src/controllers/receivingController.ts index ff892318..fbf40176 100644 --- a/backend-node/src/controllers/receivingController.ts +++ b/backend-node/src/controllers/receivingController.ts @@ -266,10 +266,10 @@ export async function create(req: AuthenticatedRequest, res: Response) { const afterQty = afterStockRes.rows[0]?.current_qty || String(inQty); await client.query( `INSERT INTO inventory_history ( - id, company_code, item_number, warehouse_code, location_code, - history_type, history_date, change_qty, after_qty, reason, - created_by, created_date - ) VALUES (gen_random_uuid()::text, $1, $2, $3, $4, '입고', NOW()::date, $5, $6, $7, $8, NOW())`, + id, company_code, item_code, warehouse_code, location_code, + transaction_type, transaction_date, quantity, balance_qty, remark, + writer, created_date + ) VALUES (gen_random_uuid()::text, $1, $2, $3, $4, '입고', NOW(), $5, $6, $7, $8, NOW())`, [companyCode, itemCode, whCode, locCode, String(inQty), afterQty, item.inbound_type || '입고', userId] ); } @@ -548,10 +548,10 @@ export async function deleteReceiving(req: AuthenticatedRequest, res: Response) const afterQty = afterStockRes.rows[0]?.current_qty || '0'; await client.query( `INSERT INTO inventory_history ( - id, company_code, item_number, warehouse_code, location_code, - history_type, history_date, change_qty, after_qty, reason, - created_by, created_date - ) VALUES (gen_random_uuid()::text, $1, $2, $3, $4, '입고취소', NOW()::date, $5, $6, '입고 삭제에 의한 롤백', $7, NOW())`, + id, company_code, item_code, warehouse_code, location_code, + transaction_type, transaction_date, quantity, balance_qty, remark, + writer, created_date + ) VALUES (gen_random_uuid()::text, $1, $2, $3, $4, '입고취소', NOW(), $5, $6, '입고 삭제에 의한 롤백', $7, NOW())`, [companyCode, itemCode, whCode, locCode, String(-inQty), afterQty, userId] ); } diff --git a/backend-node/src/routes/outboundRoutes.ts b/backend-node/src/routes/outboundRoutes.ts index f81f722b..1d9f24cf 100644 --- a/backend-node/src/routes/outboundRoutes.ts +++ b/backend-node/src/routes/outboundRoutes.ts @@ -19,6 +19,9 @@ router.get("/generate-number", outboundController.generateNumber); // 창고 목록 조회 router.get("/warehouses", outboundController.getWarehouses); +// 위치 목록 조회 +router.get("/locations", outboundController.getLocations); + // 소스 데이터: 출하지시 (판매출고) router.get("/source/shipment-instructions", outboundController.getShipmentInstructions); diff --git a/frontend/app/(main)/COMPANY_16/equipment/info/page.tsx b/frontend/app/(main)/COMPANY_16/equipment/info/page.tsx index 314db047..7ec2b68c 100644 --- a/frontend/app/(main)/COMPANY_16/equipment/info/page.tsx +++ b/frontend/app/(main)/COMPANY_16/equipment/info/page.tsx @@ -78,15 +78,17 @@ export default function EquipmentInfoPage() { const [infoForm, setInfoForm] = useState>({}); const [infoSaving, setInfoSaving] = useState(false); - // 점검항목 추가 모달 + // 점검항목 추가/수정 모달 const [inspectionModalOpen, setInspectionModalOpen] = useState(false); const [inspectionForm, setInspectionForm] = useState>({}); const [inspectionContinuous, setInspectionContinuous] = useState(false); + const [inspectionEditMode, setInspectionEditMode] = useState(false); - // 소모품 추가 모달 + // 소모품 추가/수정 모달 const [consumableModalOpen, setConsumableModalOpen] = useState(false); const [consumableForm, setConsumableForm] = useState>({}); const [consumableContinuous, setConsumableContinuous] = useState(false); + const [consumableEditMode, setConsumableEditMode] = useState(false); const [consumableItemOptions, setConsumableItemOptions] = useState([]); // 점검항목 복사 @@ -255,16 +257,44 @@ export default function EquipmentInfoPage() { // 점검항목 추가 const handleInspectionSave = async () => { if (!inspectionForm.inspection_item) { toast.error("점검항목명은 필수입니다."); return; } + if (!inspectionForm.inspection_cycle) { toast.error("점검주기는 필수입니다."); return; } + if (!inspectionForm.inspection_method) { toast.error("점검방법은 필수입니다."); return; } + const methodLabel = resolve("inspection_method", inspectionForm.inspection_method); + const isNumeric = methodLabel === "숫자" || inspectionForm.inspection_method === "숫자"; + if (isNumeric && !inspectionForm.unit) { toast.error("숫자 점검방법은 측정단위가 필수입니다."); return; } + // 기준값/오차범위 → 하한치/상한치 자동 계산 + const saveData = { ...inspectionForm }; + if (isNumeric && saveData.standard_value) { + const std = Number(saveData.standard_value) || 0; + const tol = Number(saveData.tolerance) || 0; + saveData.lower_limit = String(std - tol); + saveData.upper_limit = String(std + tol); + } + if (!isNumeric) { + saveData.unit = ""; + saveData.standard_value = ""; + saveData.tolerance = ""; + saveData.lower_limit = ""; + saveData.upper_limit = ""; + } setSaving(true); try { - await apiClient.post(`/table-management/tables/${INSPECTION_TABLE}/add`, { - ...inspectionForm, equipment_code: selectedEquip?.equipment_code, - }); - toast.success("추가되었습니다."); - if (inspectionContinuous) { - setInspectionForm({}); - } else { + if (inspectionEditMode) { + await apiClient.put(`/table-management/tables/${INSPECTION_TABLE}/edit`, { + originalData: { id: saveData.id }, updatedData: { ...saveData, equipment_code: selectedEquip?.equipment_code }, + }); + toast.success("수정되었습니다."); setInspectionModalOpen(false); + } else { + await apiClient.post(`/table-management/tables/${INSPECTION_TABLE}/add`, { + id: crypto.randomUUID(), ...saveData, equipment_code: selectedEquip?.equipment_code, + }); + toast.success("추가되었습니다."); + if (inspectionContinuous) { + setInspectionForm({}); + } else { + setInspectionModalOpen(false); + } } refreshRight(); } catch (err: any) { toast.error(err.response?.data?.message || "저장 실패"); } finally { setSaving(false); } @@ -308,14 +338,22 @@ export default function EquipmentInfoPage() { if (!consumableForm.consumable_name) { toast.error("소모품명은 필수입니다."); return; } setSaving(true); try { - await apiClient.post(`/table-management/tables/${CONSUMABLE_TABLE}/add`, { - ...consumableForm, equipment_code: selectedEquip?.equipment_code, - }); - toast.success("추가되었습니다."); - if (consumableContinuous) { - setConsumableForm({}); - } else { + if (consumableEditMode) { + await apiClient.put(`/table-management/tables/${CONSUMABLE_TABLE}/edit`, { + originalData: { id: consumableForm.id }, updatedData: { ...consumableForm, equipment_code: selectedEquip?.equipment_code }, + }); + toast.success("수정되었습니다."); setConsumableModalOpen(false); + } else { + await apiClient.post(`/table-management/tables/${CONSUMABLE_TABLE}/add`, { + id: crypto.randomUUID(), ...consumableForm, equipment_code: selectedEquip?.equipment_code, + }); + toast.success("추가되었습니다."); + if (consumableContinuous) { + setConsumableForm({}); + } else { + setConsumableModalOpen(false); + } } refreshRight(); } catch (err: any) { toast.error(err.response?.data?.message || "저장 실패"); } finally { setSaving(false); } @@ -497,7 +535,7 @@ export default function EquipmentInfoPage() {
{rightTab === "inspection" && ( <> - )} @@ -598,7 +636,13 @@ export default function EquipmentInfoPage() { {inspections.map((item) => ( - + { + const std = item.standard_value || ""; + const tol = item.tolerance || ""; + setInspectionForm({ ...item, standard_value: std, tolerance: tol }); + setInspectionEditMode(true); + setInspectionModalOpen(true); + }}> {item.inspection_item || "-"} {resolve("inspection_cycle", item.inspection_cycle)} {resolve("inspection_method", item.inspection_method)} @@ -636,7 +680,12 @@ export default function EquipmentInfoPage() { {consumables.map((item) => ( - + { + setConsumableForm({ ...item }); + setConsumableEditMode(true); + loadConsumableItems(); + setConsumableModalOpen(true); + }}> {item.consumable_name || "-"} {item.replacement_cycle || "-"} {item.unit || "-"} @@ -696,24 +745,62 @@ export default function EquipmentInfoPage() { {/* 점검항목 추가 모달 */} - 점검항목 추가{selectedEquip?.equipment_name}에 점검항목을 추가합니다. + {inspectionEditMode ? "점검항목 수정" : "점검항목 추가"}{selectedEquip?.equipment_name}에 점검항목을 {inspectionEditMode ? "수정" : "추가"}합니다.
-
- setInspectionForm((p) => ({ ...p, inspection_item: e.target.value }))} placeholder="점검항목" className="h-9" />
-
+
+ setInspectionForm((p) => ({ ...p, inspection_item: e.target.value }))} placeholder="예: 온도점검, 진동점검" className="h-9" />
+
{catSelect("inspection_cycle", inspectionForm.inspection_cycle, (v) => setInspectionForm((p) => ({ ...p, inspection_cycle: v })), "점검주기")}
-
- {catSelect("inspection_method", inspectionForm.inspection_method, (v) => setInspectionForm((p) => ({ ...p, inspection_method: v })), "점검방법")}
-
- setInspectionForm((p) => ({ ...p, lower_limit: e.target.value }))} placeholder="하한치" className="h-9" />
-
- setInspectionForm((p) => ({ ...p, upper_limit: e.target.value }))} placeholder="상한치" className="h-9" />
-
- setInspectionForm((p) => ({ ...p, unit: e.target.value }))} placeholder="단위" className="h-9" />
+
+
+ {catSelect("inspection_method", inspectionForm.inspection_method, (v) => { + const label = resolve("inspection_method", v); + const isNum = label === "숫자" || v === "숫자"; + if (!isNum) { + setInspectionForm((p) => ({ ...p, inspection_method: v, unit: "", standard_value: "", tolerance: "", lower_limit: "", upper_limit: "" })); + } else { + setInspectionForm((p) => ({ ...p, inspection_method: v })); + } + }, "점검방법")}
+ {(() => { + const methodLabel = resolve("inspection_method", inspectionForm.inspection_method); + const isNumeric = methodLabel === "숫자" || inspectionForm.inspection_method === "숫자"; + if (!isNumeric) return null; + return ( +
+ setInspectionForm((p) => ({ ...p, unit: e.target.value }))} placeholder="예: ℃, mm, V" className="h-9" />
+ ); + })()} +
+ {(() => { + const methodLabel = resolve("inspection_method", inspectionForm.inspection_method); + const isNumeric = methodLabel === "숫자" || inspectionForm.inspection_method === "숫자"; + if (!isNumeric) return null; + return ( +
+
+ setInspectionForm((p) => ({ ...p, standard_value: e.target.value }))} placeholder="기준값 입력" className="h-9" type="number" />
+
+ setInspectionForm((p) => ({ ...p, tolerance: e.target.value }))} placeholder="허용 오차범위" className="h-9" type="number" />
+
+ ); + })()}
- setInspectionForm((p) => ({ ...p, inspection_content: e.target.value }))} placeholder="점검내용" className="h-9" />
+