diff --git a/backend-node/src/controllers/bomController.ts b/backend-node/src/controllers/bomController.ts index 870833fc..7db45174 100644 --- a/backend-node/src/controllers/bomController.ts +++ b/backend-node/src/controllers/bomController.ts @@ -93,6 +93,19 @@ export async function loadBomVersion(req: Request, res: Response) { } } +export async function activateBomVersion(req: Request, res: Response) { + try { + const { bomId, versionId } = req.params; + const { tableName } = req.body || {}; + + const result = await bomService.activateBomVersion(bomId, versionId, tableName); + res.json({ success: true, data: result }); + } catch (error: any) { + logger.error("BOM 버전 사용 확정 실패", { error: error.message }); + res.status(500).json({ success: false, message: error.message }); + } +} + export async function deleteBomVersion(req: Request, res: Response) { try { const { bomId, versionId } = req.params; diff --git a/backend-node/src/routes/bomRoutes.ts b/backend-node/src/routes/bomRoutes.ts index a6d4fa10..8da360ba 100644 --- a/backend-node/src/routes/bomRoutes.ts +++ b/backend-node/src/routes/bomRoutes.ts @@ -18,6 +18,7 @@ router.post("/:bomId/history", bomController.addBomHistory); router.get("/:bomId/versions", bomController.getBomVersions); router.post("/:bomId/versions", bomController.createBomVersion); router.post("/:bomId/versions/:versionId/load", bomController.loadBomVersion); +router.post("/:bomId/versions/:versionId/activate", bomController.activateBomVersion); router.delete("/:bomId/versions/:versionId", bomController.deleteBomVersion); export default router; diff --git a/backend-node/src/services/bomService.ts b/backend-node/src/services/bomService.ts index 687326df..fbfd836d 100644 --- a/backend-node/src/services/bomService.ts +++ b/backend-node/src/services/bomService.ts @@ -112,6 +112,9 @@ export async function createBomVersion( companyCode, ]); + // BOM 헤더의 version 필드도 업데이트 + await client.query(`UPDATE bom SET version = $1 WHERE id = $2`, [versionName, bomId]); + logger.info("BOM 버전 생성", { bomId, versionName, companyCode, vTable, dTable }); return result.rows[0]; }); @@ -140,6 +143,7 @@ export async function loadBomVersion( await client.query(`DELETE FROM ${snapshotDetailTable} WHERE bom_id = $1`, [bomId]); const b = snapshot.bom; + const loadedVersionName = verRow.rows[0].version_name; await client.query( `UPDATE bom SET base_qty = $1, unit = $2, revision = $3, remark = $4 WHERE id = $5`, [b.base_qty || null, b.unit || null, b.revision || null, b.remark || null, bomId], @@ -169,12 +173,50 @@ export async function loadBomVersion( } logger.info("BOM 버전 불러오기 완료", { bomId, versionId, vTable, snapshotDetailTable }); - return { restored: true, versionName: verRow.rows[0].version_name }; + return { restored: true, versionName: loadedVersionName }; + }); +} + +export async function activateBomVersion(bomId: string, versionId: string, tableName?: string) { + const table = safeTableName(tableName || "", "bom_version"); + + return transaction(async (client) => { + const verRow = await client.query( + `SELECT version_name FROM ${table} WHERE id = $1 AND bom_id = $2`, + [versionId, bomId], + ); + if (verRow.rows.length === 0) throw new Error("버전을 찾을 수 없습니다"); + + // 기존 active -> inactive + await client.query( + `UPDATE ${table} SET status = 'inactive' WHERE bom_id = $1 AND status = 'active'`, + [bomId], + ); + // 선택한 버전 -> active + await client.query( + `UPDATE ${table} SET status = 'active' WHERE id = $1`, + [versionId], + ); + // BOM 헤더 version도 갱신 + const versionName = verRow.rows[0].version_name; + await client.query( + `UPDATE bom SET version = $1 WHERE id = $2`, + [versionName, bomId], + ); + + logger.info("BOM 버전 사용 확정", { bomId, versionId, versionName }); + return { activated: true, versionName }; }); } export async function deleteBomVersion(bomId: string, versionId: string, tableName?: string) { const table = safeTableName(tableName || "", "bom_version"); + // active 상태 버전은 삭제 불가 + const checkSql = `SELECT status FROM ${table} WHERE id = $1 AND bom_id = $2`; + const checkResult = await query(checkSql, [versionId, bomId]); + if (checkResult.length > 0 && checkResult[0].status === "active") { + throw new Error("사용중인 버전은 삭제할 수 없습니다"); + } const sql = `DELETE FROM ${table} WHERE id = $1 AND bom_id = $2 RETURNING id`; const result = await query(sql, [versionId, bomId]); return result.length > 0; diff --git a/frontend/lib/registry/components/v2-bom-tree/BomDetailEditModal.tsx b/frontend/lib/registry/components/v2-bom-tree/BomDetailEditModal.tsx index 04ce8ebb..cfff4a0c 100644 --- a/frontend/lib/registry/components/v2-bom-tree/BomDetailEditModal.tsx +++ b/frontend/lib/registry/components/v2-bom-tree/BomDetailEditModal.tsx @@ -14,7 +14,7 @@ import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { Loader2 } from "lucide-react"; -import apiClient from "@/lib/api/client"; +import { apiClient } from "@/lib/api/client"; interface BomDetailEditModalProps { open: boolean; diff --git a/frontend/lib/registry/components/v2-bom-tree/BomHistoryModal.tsx b/frontend/lib/registry/components/v2-bom-tree/BomHistoryModal.tsx index 9d66a8df..26119aaa 100644 --- a/frontend/lib/registry/components/v2-bom-tree/BomHistoryModal.tsx +++ b/frontend/lib/registry/components/v2-bom-tree/BomHistoryModal.tsx @@ -12,7 +12,7 @@ import { import { Button } from "@/components/ui/button"; import { Loader2 } from "lucide-react"; import { cn } from "@/lib/utils"; -import apiClient from "@/lib/api/client"; +import { apiClient } from "@/lib/api/client"; interface BomHistoryItem { id: string; diff --git a/frontend/lib/registry/components/v2-bom-tree/BomTreeComponent.tsx b/frontend/lib/registry/components/v2-bom-tree/BomTreeComponent.tsx index 88461200..6061f4cd 100644 --- a/frontend/lib/registry/components/v2-bom-tree/BomTreeComponent.tsx +++ b/frontend/lib/registry/components/v2-bom-tree/BomTreeComponent.tsx @@ -357,19 +357,92 @@ export function BomTreeComponent({ if (isDesignMode) { const configuredColumns = (config.columns || []).filter((c: TreeColumnDef) => !c.hidden); + const previewSampleValue = (col: TreeColumnDef, rowIdx: number): React.ReactNode => { + if (col.key === "level") return rowIdx === 0 ? "0" : "1"; + if (col.key.includes("type") || col.key.includes("division")) { + const badge = rowIdx === 0 + ? { bg: "bg-blue-50 text-blue-500 ring-blue-200", label: "제품" } + : { bg: "bg-amber-50 text-amber-500 ring-amber-200", label: "반제품" }; + return ( + + {badge.label} + + ); + } + if (col.key.includes("quantity") || col.key.includes("qty")) return rowIdx === 0 ? "30" : "3"; + if (col.key.includes("unit")) return "EA"; + if (col.key.includes("process")) { + const badge = rowIdx === 0 + ? { bg: "bg-emerald-50 text-emerald-600 ring-emerald-200", label: "제조" } + : { bg: "bg-purple-50 text-purple-600 ring-purple-200", label: "외주" }; + return ( + + {badge.label} + + ); + } + return `예시${rowIdx + 1}`; + }; + return ( -
설정 패널 > 컬럼 탭에서 표시할 컬럼을 선택하세요
| - {configuredColumns.map((col: TreeColumnDef) => ( - | - {col.title || col.key} - | - ))} ++ {configuredColumns.map((col: TreeColumnDef) => { + const centered = ["quantity", "loss_rate", "base_qty", "revision", "seq_no", "unit", "level"].includes(col.key) + || col.key.includes("qty") || col.key.includes("quantity"); + return ( + | + {col.title || col.key} + | + ); + })}
|---|---|---|---|
|
- | |||
|
+ |
- {configuredColumns.map((col: TreeColumnDef, i: number) => (
- - {col.key === "level" ? "0" : col.key.includes("type") ? ( - 제품 - ) : col.key.includes("quantity") || col.key.includes("qty") ? "30" : `예시${i + 1}`} - | - ))} + {configuredColumns.map((col: TreeColumnDef) => { + const centered = ["quantity", "loss_rate", "base_qty", "revision", "seq_no", "unit", "level"].includes(col.key) + || col.key.includes("qty") || col.key.includes("quantity"); + return ( ++ {previewSampleValue(col, 0)} + | + ); + })}|
| - + {/* 1레벨 자식 */} + | |||
|
+ |
- {configuredColumns.map((col: TreeColumnDef, i: number) => (
- - {col.key === "level" ? "1" : col.key.includes("type") ? ( - 반제품 - ) : col.key.includes("quantity") || col.key.includes("qty") ? "3" : `예시${i + 1}`} - | - ))} + {configuredColumns.map((col: TreeColumnDef) => { + const centered = ["quantity", "loss_rate", "base_qty", "revision", "seq_no", "unit", "level"].includes(col.key) + || col.key.includes("qty") || col.key.includes("quantity"); + return ( ++ {previewSampleValue(col, 1)} + | + ); + })}