From afc66a4971cc55e505b5b529251ff23f64edf31d Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Thu, 26 Feb 2026 17:07:53 +0900 Subject: [PATCH 1/2] feat: Enhance SelectedItemsDetailInputComponent with improved FK mapping and performance optimizations - Implemented automatic detection of sourceKeyField based on component configuration, enhancing data handling flexibility. - Updated SelectedItemsDetailInputConfigPanel to support automatic FK detection and mapping, streamlining configuration. - Improved database connection logic for DATE types to prevent timezone-related issues. - Optimized memoization and state management for better overall component performance and user experience. --- bom-restore-verify.mjs | 85 +++++++++++++++++++ .../app/(main)/screen/[screenCode]/page.tsx | 14 +-- 2 files changed, 93 insertions(+), 6 deletions(-) create mode 100644 bom-restore-verify.mjs diff --git a/bom-restore-verify.mjs b/bom-restore-verify.mjs new file mode 100644 index 00000000..15407bde --- /dev/null +++ b/bom-restore-verify.mjs @@ -0,0 +1,85 @@ +/** + * BOM Screen - Restoration Verification + * Screen 4168 - verify split panel, BOM list, and tree with child items + */ +import { chromium } from 'playwright'; +import { mkdirSync, existsSync } from 'fs'; +import { join } from 'path'; + +const SCREENSHOT_DIR = join(process.cwd(), 'bom-detail-test-screenshots'); + +async function ensureDir(dir) { + if (!existsSync(dir)) mkdirSync(dir, { recursive: true }); +} + +async function screenshot(page, name) { + ensureDir(SCREENSHOT_DIR); + await page.screenshot({ path: join(SCREENSHOT_DIR, `${name}.png`), fullPage: true }); + console.log(` [Screenshot] ${name}.png`); +} + +async function sleep(ms) { + return new Promise((r) => setTimeout(r, ms)); +} + +async function main() { + const browser = await chromium.launch({ headless: true }); + const page = await browser.newPage({ viewport: { width: 1400, height: 900 } }); + + try { + console.log('\n--- Step 1-2: Login ---'); + await page.goto('http://localhost:9771/login', { waitUntil: 'load', timeout: 45000 }); + await page.locator('input[type="text"], input[placeholder*="ID"]').first().fill('topseal_admin'); + await page.locator('input[type="password"]').first().fill('qlalfqjsgh11'); + await Promise.all([ + page.waitForNavigation({ waitUntil: 'domcontentloaded', timeout: 20000 }).catch(() => {}), + page.locator('button:has-text("로그인")').first().click(), + ]); + await sleep(3000); + + console.log('\n--- Step 4-5: Navigate to screen 4168 ---'); + await page.goto('http://localhost:9771/screens/4168', { waitUntil: 'load', timeout: 45000 }); + await sleep(5000); + + console.log('\n--- Step 6: Screenshot after load ---'); + await screenshot(page, '10-bom-4168-initial'); + + const hasBomList = (await page.locator('text="BOM 목록"').count()) > 0; + const hasSplitPanel = (await page.locator('text="BOM 상세정보"').count()) > 0 || hasBomList; + const rowCount = await page.locator('table tbody tr').count(); + const hasBomRows = rowCount > 0; + + console.log('\n========== INITIAL STATE (Step 7) =========='); + console.log('BOM management screen loaded:', hasBomList || hasSplitPanel ? 'YES' : 'CHECK'); + console.log('Split panel (BOM list left):', hasSplitPanel ? 'YES' : 'NO'); + console.log('BOM data rows visible:', hasBomRows ? `YES (${rowCount} rows)` : 'NO'); + + if (hasBomRows) { + console.log('\n--- Step 8-9: Click first row ---'); + await page.locator('table tbody tr').first().click(); + await sleep(5000); + + console.log('\n--- Step 10: Screenshot after row click ---'); + await screenshot(page, '11-bom-4168-after-click'); + + const noDataMsg = (await page.locator('text="등록된 하위 품목이 없습니다"').count()) > 0; + const treeArea = page.locator('div:has-text("BOM 구성"), div:has-text("BOM 상세정보")').first(); + const treeText = (await treeArea.textContent().catch(() => '') || '').substring(0, 600); + const hasChildItems = !noDataMsg && (treeText.includes('품번') || treeText.includes('레벨') || treeText.length > 150); + + console.log('\n========== AFTER ROW CLICK (Step 11) =========='); + console.log('BOM tree shows child items:', hasChildItems ? 'YES' : noDataMsg ? 'NO (empty message)' : 'CHECK'); + console.log('Tree preview:', treeText.substring(0, 300) + (treeText.length > 300 ? '...' : '')); + } else { + console.log('\n--- No BOM rows to click ---'); + } + + } catch (err) { + console.error('Error:', err.message); + try { await page.screenshot({ path: join(SCREENSHOT_DIR, '99-error.png'), fullPage: true }); } catch (e) {} + } finally { + await browser.close(); + } +} + +main(); diff --git a/frontend/app/(main)/screen/[screenCode]/page.tsx b/frontend/app/(main)/screen/[screenCode]/page.tsx index 0817065e..64c1bb34 100644 --- a/frontend/app/(main)/screen/[screenCode]/page.tsx +++ b/frontend/app/(main)/screen/[screenCode]/page.tsx @@ -6,7 +6,7 @@ import { Loader2 } from "lucide-react"; import { apiClient } from "@/lib/api/client"; /** - * /screen/COMPANY_7_167 → /screens/4153 리다이렉트 + * /screen/{screenCode} → /screens/{screenId} 리다이렉트 * 메뉴 URL이 screenCode 기반이므로, screenId로 변환 후 이동 */ export default function ScreenCodeRedirectPage() { @@ -26,12 +26,14 @@ export default function ScreenCodeRedirectPage() { const resolve = async () => { try { const res = await apiClient.get("/screen-management/screens", { - params: { screenCode }, + params: { searchTerm: screenCode, size: 50 }, }); - const screens = res.data?.data || []; - if (screens.length > 0) { - const id = screens[0].screenId || screens[0].screen_id; - router.replace(`/screens/${id}`); + const items = res.data?.data?.data || res.data?.data || []; + const arr = Array.isArray(items) ? items : []; + const exact = arr.find((s: any) => s.screenCode === screenCode); + const target = exact || arr[0]; + if (target) { + router.replace(`/screens/${target.screenId || target.screen_id}`); } else { router.replace("/"); } From 385a10e2e77a61596b8c14dbcca6acd477f17948 Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Thu, 26 Feb 2026 20:48:56 +0900 Subject: [PATCH 2/2] feat: Add BOM version initialization feature and enhance version handling - Implemented a new endpoint to initialize BOM versions, automatically creating the first version and updating related details. - Enhanced the BOM service to include logic for version name handling and duplication checks during version creation. - Updated the BOM controller to support the new initialization functionality, improving BOM management capabilities. - Improved the BOM version modal to allow users to specify version names during creation, enhancing user experience and flexibility. --- backend-node/src/controllers/bomController.ts | 18 ++- backend-node/src/routes/bomRoutes.ts | 1 + backend-node/src/services/bomService.ts | 90 ++++++++++-- .../BomItemEditorComponent.tsx | 129 +++++++++++------- .../v2-bom-tree/BomDetailEditModal.tsx | 55 +++++++- .../v2-bom-tree/BomTreeComponent.tsx | 40 +++++- .../v2-bom-tree/BomVersionModal.tsx | 70 ++++++++-- 7 files changed, 321 insertions(+), 82 deletions(-) diff --git a/backend-node/src/controllers/bomController.ts b/backend-node/src/controllers/bomController.ts index 8355b148..3508fca4 100644 --- a/backend-node/src/controllers/bomController.ts +++ b/backend-node/src/controllers/bomController.ts @@ -92,9 +92,9 @@ export async function createBomVersion(req: Request, res: Response) { const { bomId } = req.params; const companyCode = (req as any).user?.companyCode || "*"; const createdBy = (req as any).user?.userName || (req as any).user?.userId || ""; - const { tableName, detailTable } = req.body || {}; + const { tableName, detailTable, versionName } = req.body || {}; - const result = await bomService.createBomVersion(bomId, companyCode, createdBy, tableName, detailTable); + const result = await bomService.createBomVersion(bomId, companyCode, createdBy, tableName, detailTable, versionName); res.json({ success: true, data: result }); } catch (error: any) { logger.error("BOM 버전 생성 실패", { error: error.message }); @@ -129,6 +129,20 @@ export async function activateBomVersion(req: Request, res: Response) { } } +export async function initializeBomVersion(req: Request, res: Response) { + try { + const { bomId } = req.params; + const companyCode = (req as any).user?.companyCode || "*"; + const createdBy = (req as any).user?.userName || (req as any).user?.userId || ""; + + const result = await bomService.initializeBomVersion(bomId, companyCode, createdBy); + 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 f6e3ee62..4aa8838d 100644 --- a/backend-node/src/routes/bomRoutes.ts +++ b/backend-node/src/routes/bomRoutes.ts @@ -20,6 +20,7 @@ router.post("/:bomId/history", bomController.addBomHistory); // 버전 router.get("/:bomId/versions", bomController.getBomVersions); router.post("/:bomId/versions", bomController.createBomVersion); +router.post("/:bomId/initialize-version", bomController.initializeBomVersion); router.post("/:bomId/versions/:versionId/load", bomController.loadBomVersion); router.post("/:bomId/versions/:versionId/activate", bomController.activateBomVersion); router.delete("/:bomId/versions/:versionId", bomController.deleteBomVersion); diff --git a/backend-node/src/services/bomService.ts b/backend-node/src/services/bomService.ts index 89da38a9..b5cff246 100644 --- a/backend-node/src/services/bomService.ts +++ b/backend-node/src/services/bomService.ts @@ -98,6 +98,7 @@ export async function getBomVersions(bomId: string, companyCode: string, tableNa export async function createBomVersion( bomId: string, companyCode: string, createdBy: string, versionTableName?: string, detailTableName?: string, + inputVersionName?: string, ) { const vTable = safeTableName(versionTableName || "", "bom_version"); const dTable = safeTableName(detailTableName || "", "bom_detail"); @@ -107,17 +108,24 @@ export async function createBomVersion( if (bomRow.rows.length === 0) throw new Error("BOM을 찾을 수 없습니다"); const bomData = bomRow.rows[0]; - // 다음 버전 번호 결정 - const lastVersion = await client.query( - `SELECT version_name FROM ${vTable} WHERE bom_id = $1 ORDER BY created_date DESC LIMIT 1`, - [bomId], - ); - let nextVersionNum = 1; - if (lastVersion.rows.length > 0) { - const parsed = parseFloat(lastVersion.rows[0].version_name); - if (!isNaN(parsed)) nextVersionNum = Math.floor(parsed) + 1; + // 버전명: 사용자 입력 > 순번 자동 생성 + let versionName = inputVersionName?.trim(); + if (!versionName) { + const countResult = await client.query( + `SELECT COUNT(*)::int as cnt FROM ${vTable} WHERE bom_id = $1`, + [bomId], + ); + versionName = `${(countResult.rows[0].cnt || 0) + 1}.0`; + } + + // 중복 체크 + const dupCheck = await client.query( + `SELECT id FROM ${vTable} WHERE bom_id = $1 AND version_name = $2`, + [bomId, versionName], + ); + if (dupCheck.rows.length > 0) { + throw new Error(`이미 존재하는 버전명입니다: ${versionName}`); } - const versionName = `${nextVersionNum}.0`; // 새 버전 레코드 생성 (snapshot_data 없이) const insertSql = ` @@ -249,6 +257,68 @@ export async function activateBomVersion(bomId: string, versionId: string, table }); } +/** + * 신규 BOM 초기화: 첫 번째 버전 자동 생성 + version_id null인 디테일 보정 + * BOM 헤더의 version 필드를 그대로 버전명으로 사용 (사용자 입력값 존중) + */ +export async function initializeBomVersion( + bomId: string, companyCode: string, createdBy: string, +) { + return transaction(async (client) => { + const bomRow = await client.query(`SELECT * FROM bom WHERE id = $1`, [bomId]); + if (bomRow.rows.length === 0) throw new Error("BOM을 찾을 수 없습니다"); + const bomData = bomRow.rows[0]; + + if (bomData.current_version_id) { + await client.query( + `UPDATE bom_detail SET version_id = $1 WHERE bom_id = $2 AND version_id IS NULL`, + [bomData.current_version_id, bomId], + ); + return { versionId: bomData.current_version_id, created: false }; + } + + // 이미 버전 레코드가 존재하는지 확인 (동시 호출 방지) + const existingVersion = await client.query( + `SELECT id, version_name FROM bom_version WHERE bom_id = $1 ORDER BY created_date ASC LIMIT 1`, + [bomId], + ); + if (existingVersion.rows.length > 0) { + const existId = existingVersion.rows[0].id; + await client.query( + `UPDATE bom_detail SET version_id = $1 WHERE bom_id = $2 AND version_id IS NULL`, + [existId, bomId], + ); + await client.query( + `UPDATE bom SET current_version_id = $1 WHERE id = $2 AND current_version_id IS NULL`, + [existId, bomId], + ); + return { versionId: existId, created: false }; + } + + const versionName = bomData.version || "1.0"; + + const versionResult = await client.query( + `INSERT INTO bom_version (bom_id, version_name, revision, status, created_by, company_code) + VALUES ($1, $2, 0, 'active', $3, $4) RETURNING id`, + [bomId, versionName, createdBy, companyCode], + ); + const versionId = versionResult.rows[0].id; + + const updated = await client.query( + `UPDATE bom_detail SET version_id = $1 WHERE bom_id = $2 AND version_id IS NULL`, + [versionId, bomId], + ); + + await client.query( + `UPDATE bom SET current_version_id = $1 WHERE id = $2`, + [versionId, bomId], + ); + + logger.info("BOM 초기 버전 생성", { bomId, versionId, versionName, updatedDetails: updated.rowCount }); + return { versionId, versionName, created: true }; + }); +} + /** * 버전 삭제: 해당 version_id의 bom_detail 행도 함께 삭제 */ diff --git a/frontend/lib/registry/components/v2-bom-item-editor/BomItemEditorComponent.tsx b/frontend/lib/registry/components/v2-bom-item-editor/BomItemEditorComponent.tsx index e4521ac0..75c1909b 100644 --- a/frontend/lib/registry/components/v2-bom-item-editor/BomItemEditorComponent.tsx +++ b/frontend/lib/registry/components/v2-bom-item-editor/BomItemEditorComponent.tsx @@ -86,6 +86,7 @@ interface ItemSearchModalProps { onClose: () => void; onSelect: (items: ItemInfo[]) => void; companyCode?: string; + existingItemIds?: Set; } function ItemSearchModal({ @@ -93,6 +94,7 @@ function ItemSearchModal({ onClose, onSelect, companyCode, + existingItemIds, }: ItemSearchModalProps) { const [searchText, setSearchText] = useState(""); const [items, setItems] = useState([]); @@ -182,7 +184,7 @@ function ItemSearchModal({ ) : ( - + - {items.map((item) => ( - { - setSelectedItems((prev) => { - const next = new Set(prev); - if (next.has(item.id)) next.delete(item.id); - else next.add(item.id); - return next; - }); - }} - className={cn( - "cursor-pointer border-t transition-colors", - selectedItems.has(item.id) ? "bg-primary/10" : "hover:bg-accent", - )} - > - - - - - - - ))} + {items.map((item) => { + const alreadyAdded = existingItemIds?.has(item.id) || false; + return ( + { + if (alreadyAdded) return; + setSelectedItems((prev) => { + const next = new Set(prev); + if (next.has(item.id)) next.delete(item.id); + else next.add(item.id); + return next; + }); + }} + className={cn( + "border-t transition-colors", + alreadyAdded + ? "cursor-not-allowed opacity-40" + : "cursor-pointer", + !alreadyAdded && selectedItems.has(item.id) ? "bg-primary/10" : !alreadyAdded ? "hover:bg-accent" : "", + )} + > + + + + + + + ); + })}
e.stopPropagation()}> - { - setSelectedItems((prev) => { - const next = new Set(prev); - if (checked) next.add(item.id); - else next.delete(item.id); - return next; - }); - }} - /> - - {item.item_number} - {item.item_name}{item.type}{item.unit}
e.stopPropagation()}> + { + if (alreadyAdded) return; + setSelectedItems((prev) => { + const next = new Set(prev); + if (checked) next.add(item.id); + else next.delete(item.id); + return next; + }); + }} + /> + + {item.item_number} + {alreadyAdded && (추가됨)} + {item.item_name}{item.type}{item.unit}
)} @@ -739,37 +751,40 @@ export function BomItemEditorComponent({ [originalNotifyChange, markChanged], ); + const handleSaveAllRef = React.useRef<(() => Promise) | null>(null); + // EditModal 저장 시 beforeFormSave 이벤트로 디테일 데이터도 함께 저장 useEffect(() => { if (isDesignMode || !bomId) return; const handler = (e: Event) => { const detail = (e as CustomEvent).detail; - console.log("[BomItemEditor] beforeFormSave 이벤트 수신:", { - bomId, - treeDataLength: treeData.length, - hasRef: !!handleSaveAllRef.current, - }); - if (treeData.length > 0 && handleSaveAllRef.current) { + if (handleSaveAllRef.current) { const savePromise = handleSaveAllRef.current(); if (detail?.pendingPromises) { detail.pendingPromises.push(savePromise); - console.log("[BomItemEditor] pendingPromises에 저장 Promise 등록 완료"); } } }; window.addEventListener("beforeFormSave", handler); - console.log("[BomItemEditor] beforeFormSave 리스너 등록:", { bomId, isDesignMode }); return () => window.removeEventListener("beforeFormSave", handler); - }, [isDesignMode, bomId, treeData.length]); - - const handleSaveAllRef = React.useRef<(() => Promise) | null>(null); + }, [isDesignMode, bomId]); const handleSaveAll = useCallback(async () => { if (!bomId) return; setSaving(true); try { - // 저장 시점에도 최신 version_id 조회 - const saveVersionId = await fetchCurrentVersionId(bomId) || propsVersionId; + // version_id 확보: 없으면 서버에서 자동 초기화 + let saveVersionId = await fetchCurrentVersionId(bomId) || propsVersionId; + if (!saveVersionId) { + try { + const initRes = await apiClient.post(`/bom/${bomId}/initialize-version`); + if (initRes.data?.success && initRes.data.data?.versionId) { + saveVersionId = initRes.data.data.versionId; + } + } catch (e) { + console.error("[BomItemEditor] 버전 초기화 실패:", e); + } + } const collectAll = (nodes: BomItemNode[], parentRealId: string | null, level: number): any[] => { const result: any[] = []; @@ -1338,6 +1353,18 @@ export function BomItemEditorComponent({ onClose={() => setItemSearchOpen(false)} onSelect={handleItemSelect} companyCode={companyCode} + existingItemIds={useMemo(() => { + const ids = new Set(); + const collect = (nodes: BomItemNode[]) => { + for (const n of nodes) { + const fk = n.data[cfg.dataSource?.foreignKey || "child_item_id"]; + if (fk) ids.add(fk); + collect(n.children); + } + }; + collect(treeData); + return ids; + }, [treeData, cfg])} /> ); diff --git a/frontend/lib/registry/components/v2-bom-tree/BomDetailEditModal.tsx b/frontend/lib/registry/components/v2-bom-tree/BomDetailEditModal.tsx index cfff4a0c..6b5d4a40 100644 --- a/frontend/lib/registry/components/v2-bom-tree/BomDetailEditModal.tsx +++ b/frontend/lib/registry/components/v2-bom-tree/BomDetailEditModal.tsx @@ -13,6 +13,13 @@ import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; import { Loader2 } from "lucide-react"; import { apiClient } from "@/lib/api/client"; @@ -35,6 +42,20 @@ export function BomDetailEditModal({ }: BomDetailEditModalProps) { const [formData, setFormData] = useState>({}); const [saving, setSaving] = useState(false); + const [processOptions, setProcessOptions] = useState<{ value: string; label: string }[]>([]); + + useEffect(() => { + if (open && !isRootNode) { + apiClient.get("/table-categories/bom_detail/process_type/values") + .then((res) => { + const values = res.data?.data || []; + if (values.length > 0) { + setProcessOptions(values.map((v: any) => ({ value: v.value_code, label: v.value_label }))); + } + }) + .catch(() => { /* 카테고리 없으면 빈 배열 유지 */ }); + } + }, [open, isRootNode]); useEffect(() => { if (node && open) { @@ -67,11 +88,15 @@ export function BomDetailEditModal({ try { const targetTable = isRootNode ? "bom" : tableName; const realId = isRootNode ? node.id?.replace("__root_", "") : node.id; - await apiClient.put(`/table-management/tables/${targetTable}/${realId}`, formData); + await apiClient.put(`/table-management/tables/${targetTable}/edit`, { + originalData: { id: realId }, + updatedData: { id: realId, ...formData }, + }); onSaved?.(); onOpenChange(false); } catch (error) { console.error("[BomDetailEdit] 저장 실패:", error); + alert("저장 중 오류가 발생했습니다."); } finally { setSaving(false); } @@ -139,12 +164,28 @@ export function BomDetailEditModal({
- handleChange("process_type", e.target.value)} - placeholder="예: 조립공정" - className="mt-1 h-8 text-xs sm:h-10 sm:text-sm" - /> + {processOptions.length > 0 ? ( + + ) : ( + handleChange("process_type", e.target.value)} + placeholder="예: 조립공정" + className="mt-1 h-8 text-xs sm:h-10 sm:text-sm" + /> + )}
diff --git a/frontend/lib/registry/components/v2-bom-tree/BomTreeComponent.tsx b/frontend/lib/registry/components/v2-bom-tree/BomTreeComponent.tsx index 957b8d85..5234a74d 100644 --- a/frontend/lib/registry/components/v2-bom-tree/BomTreeComponent.tsx +++ b/frontend/lib/registry/components/v2-bom-tree/BomTreeComponent.tsx @@ -138,6 +138,23 @@ export function BomTreeComponent({ const showHistory = features.showHistory !== false; const showVersion = features.showVersion !== false; + // 카테고리 라벨 캐시 (process_type 등) + const [categoryLabels, setCategoryLabels] = useState>>({}); + useEffect(() => { + const loadLabels = async () => { + try { + const res = await apiClient.get(`/table-categories/${detailTable}/process_type/values`); + const vals = res.data?.data || []; + if (vals.length > 0) { + const map: Record = {}; + vals.forEach((v: any) => { map[v.value_code] = v.value_label; }); + setCategoryLabels((prev) => ({ ...prev, process_type: map })); + } + } catch { /* 무시 */ } + }; + loadLabels(); + }, [detailTable]); + // ─── 데이터 로드 ─── // BOM 헤더 데이터로 가상 0레벨 루트 노드 생성 @@ -168,7 +185,18 @@ export function BomTreeComponent({ setLoading(true); try { const searchFilter: Record = { [foreignKey]: bomId }; - const versionId = headerData?.current_version_id; + let versionId = headerData?.current_version_id; + + // version_id가 없으면 서버에서 자동 초기화 + if (!versionId) { + try { + const initRes = await apiClient.post(`/bom/${bomId}/initialize-version`); + if (initRes.data?.success && initRes.data.data?.versionId) { + versionId = initRes.data.data.versionId; + } + } catch { /* 무시 */ } + } + if (versionId) { searchFilter.version_id = versionId; } @@ -461,6 +489,11 @@ export function BomTreeComponent({ return {value || "-"}; } + if (col.key === "status") { + const statusMap: Record = { active: "사용", inactive: "미사용", developing: "개발중" }; + return {statusMap[String(value)] || value || "-"}; + } + if (col.key === "quantity" || col.key === "base_qty") { return ( @@ -469,6 +502,11 @@ export function BomTreeComponent({ ); } + if (col.key === "process_type" && value) { + const label = categoryLabels.process_type?.[String(value)] || String(value); + return {label}; + } + if (col.key === "loss_rate") { const num = Number(value); if (!num) return -; diff --git a/frontend/lib/registry/components/v2-bom-tree/BomVersionModal.tsx b/frontend/lib/registry/components/v2-bom-tree/BomVersionModal.tsx index d36bfe6e..48c27cc9 100644 --- a/frontend/lib/registry/components/v2-bom-tree/BomVersionModal.tsx +++ b/frontend/lib/registry/components/v2-bom-tree/BomVersionModal.tsx @@ -43,6 +43,8 @@ export function BomVersionModal({ open, onOpenChange, bomId, tableName = "bom_ve const [loading, setLoading] = useState(false); const [creating, setCreating] = useState(false); const [actionId, setActionId] = useState(null); + const [newVersionName, setNewVersionName] = useState(""); + const [showNewInput, setShowNewInput] = useState(false); useEffect(() => { if (open && bomId) loadVersions(); @@ -63,11 +65,26 @@ export function BomVersionModal({ open, onOpenChange, bomId, tableName = "bom_ve const handleCreateVersion = async () => { if (!bomId) return; + const trimmed = newVersionName.trim(); + if (!trimmed) { + alert("버전명을 입력해주세요."); + return; + } setCreating(true); try { - const res = await apiClient.post(`/bom/${bomId}/versions`, { tableName, detailTable }); - if (res.data?.success) loadVersions(); - } catch (error) { + const res = await apiClient.post(`/bom/${bomId}/versions`, { + tableName, detailTable, versionName: trimmed, + }); + if (res.data?.success) { + setNewVersionName(""); + setShowNewInput(false); + loadVersions(); + } else { + alert(res.data?.message || "버전 생성 실패"); + } + } catch (error: any) { + const msg = error.response?.data?.message || "버전 생성 실패"; + alert(msg); console.error("[BomVersion] 생성 실패:", error); } finally { setCreating(false); @@ -230,15 +247,46 @@ export function BomVersionModal({ open, onOpenChange, bomId, tableName = "bom_ve )}
+ {showNewInput && ( +
+ setNewVersionName(e.target.value)} + onKeyDown={(e) => e.key === "Enter" && handleCreateVersion()} + placeholder="버전명 입력 (예: 2.0, B, 개선판)" + className="h-8 flex-1 rounded-md border px-3 text-xs focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring sm:h-10 sm:text-sm" + autoFocus + /> + + +
+ )} + - + {!showNewInput && ( + + )}