diff --git a/backend-node/src/controllers/receivingController.ts b/backend-node/src/controllers/receivingController.ts index 3a1aeec4..f0a3342c 100644 --- a/backend-node/src/controllers/receivingController.ts +++ b/backend-node/src/controllers/receivingController.ts @@ -783,7 +783,7 @@ export async function getShipments(req: AuthenticatedRequest, res: Response) { export async function getItems(req: AuthenticatedRequest, res: Response) { try { const companyCode = req.user!.companyCode; - const { keyword, page, pageSize } = req.query; + const { keyword, page, pageSize, division } = req.query; const currentPage = Math.max(1, Number(page) || 1); const limit = Math.min(500, Math.max(1, Number(pageSize) || 20)); const offset = (currentPage - 1) * limit; @@ -800,6 +800,12 @@ export async function getItems(req: AuthenticatedRequest, res: Response) { paramIdx++; } + if (division) { + conditions.push(`division ILIKE $${paramIdx}`); + params.push(`%${division}%`); + paramIdx++; + } + const whereClause = conditions.join(" AND "); const pool = getPool(); diff --git a/frontend/app/(main)/COMPANY_7/equipment/info/page.tsx b/frontend/app/(main)/COMPANY_7/equipment/info/page.tsx index 9bb5fede..6c843483 100644 --- a/frontend/app/(main)/COMPANY_7/equipment/info/page.tsx +++ b/frontend/app/(main)/COMPANY_7/equipment/info/page.tsx @@ -105,9 +105,11 @@ export default function EquipmentInfoPage() { const [inspectionModalOpen, setInspectionModalOpen] = useState(false); const [inspectionForm, setInspectionForm] = useState>({}); + const [inspectionContinuous, setInspectionContinuous] = useState(false); const [consumableModalOpen, setConsumableModalOpen] = useState(false); const [consumableForm, setConsumableForm] = useState>({}); + const [consumableContinuous, setConsumableContinuous] = useState(false); const [consumableItemOptions, setConsumableItemOptions] = useState([]); // 점검항목 복사 @@ -294,7 +296,13 @@ export default function EquipmentInfoPage() { await apiClient.post(`/table-management/tables/${INSPECTION_TABLE}/add`, { ...inspectionForm, equipment_code: selectedEquip?.equipment_code, }); - toast.success("추가되었습니다."); setInspectionModalOpen(false); refreshRight(); + toast.success("추가되었습니다."); + if (inspectionContinuous) { + setInspectionForm({}); + } else { + setInspectionModalOpen(false); + } + refreshRight(); } catch (err: any) { toast.error(err.response?.data?.message || "저장 실패"); } finally { setSaving(false); } }; @@ -347,7 +355,13 @@ export default function EquipmentInfoPage() { await apiClient.post(`/table-management/tables/${CONSUMABLE_TABLE}/add`, { ...consumableForm, equipment_code: selectedEquip?.equipment_code, }); - toast.success("추가되었습니다."); setConsumableModalOpen(false); refreshRight(); + toast.success("추가되었습니다."); + if (consumableContinuous) { + setConsumableForm({}); + } else { + setConsumableModalOpen(false); + } + refreshRight(); } catch (err: any) { toast.error(err.response?.data?.message || "저장 실패"); } finally { setSaving(false); } }; @@ -609,8 +623,16 @@ export default function EquipmentInfoPage() {
setInspectionForm((p) => ({ ...p, inspection_content: e.target.value }))} placeholder="점검내용" className="h-9" />
- - + + +
+ + +
+
@@ -659,8 +681,16 @@ export default function EquipmentInfoPage() { setConsumableForm((p) => ({ ...p, image_path: v }))} tableName={CONSUMABLE_TABLE} columnName="image_path" /> - - + + +
+ + +
+
diff --git a/frontend/app/(main)/COMPANY_7/logistics/receiving/page.tsx b/frontend/app/(main)/COMPANY_7/logistics/receiving/page.tsx index 6e493a5c..4222a23d 100644 --- a/frontend/app/(main)/COMPANY_7/logistics/receiving/page.tsx +++ b/frontend/app/(main)/COMPANY_7/logistics/receiving/page.tsx @@ -42,6 +42,7 @@ import { ChevronsRight, } from "lucide-react"; import { cn } from "@/lib/utils"; +import { apiClient } from "@/lib/api/client"; import { getReceivingList, createReceiving, @@ -140,13 +141,23 @@ export default function ReceivingPage() { const [sourcePageSize, setSourcePageSize] = useState(20); const [sourceTotalCount, setSourceTotalCount] = useState(0); - // 날짜 초기화 + // 구매관리 division 코드 (라벨 기준 조회) + const [purchaseDivisionCode, setPurchaseDivisionCode] = useState(""); + + // 날짜 초기화 + 구매관리 division 코드 로드 useEffect(() => { const today = new Date(); const threeMonthsAgo = new Date(today); threeMonthsAgo.setMonth(threeMonthsAgo.getMonth() - 3); setSearchDateFrom(threeMonthsAgo.toISOString().split("T")[0]); setSearchDateTo(today.toISOString().split("T")[0]); + + // division 카테고리에서 "구매관리" 라벨의 코드 조회 + apiClient.get("/table-categories/item_info/division/values").then((res) => { + const vals = res.data?.data || []; + const found = vals.find((v: any) => (v.value_label || v.label) === "구매관리"); + if (found) setPurchaseDivisionCode(found.value_code || found.code); + }).catch(() => {}); }, []); // 목록 조회 @@ -243,7 +254,7 @@ export default function ReceivingPage() { setSourceTotalCount(res.totalCount || 0); } } else { - const res = await getItemSources(params); + const res = await getItemSources({ ...params, division: purchaseDivisionCode || undefined }); if (res.success) { setItems(res.data); setSourceTotalCount(res.totalCount || 0); diff --git a/frontend/app/(main)/COMPANY_7/sales/customer/page.tsx b/frontend/app/(main)/COMPANY_7/sales/customer/page.tsx index 80995df0..663ad4f3 100644 --- a/frontend/app/(main)/COMPANY_7/sales/customer/page.tsx +++ b/frontend/app/(main)/COMPANY_7/sales/customer/page.tsx @@ -95,8 +95,10 @@ export default function CustomerManagementPage() { // 우측: 품목 단가 const [priceItems, setPriceItems] = useState([]); const [priceLoading, setPriceLoading] = useState(false); + const [priceCheckedIds, setPriceCheckedIds] = useState([]); // 우측: 납품처 const [deliveryItems, setDeliveryItems] = useState([]); + const [deliveryCheckedIds, setDeliveryCheckedIds] = useState([]); // 품목 편집 데이터 (더블클릭 시 — 상세 입력 모달 재활용) const [editItemData, setEditItemData] = useState(null); @@ -254,7 +256,8 @@ export default function CustomerManagementPage() { const selectedCustomer = customers.find((c) => c.id === selectedCustomerId); useEffect(() => { - if (!selectedCustomer?.customer_code) { setPriceItems([]); return; } + if (!selectedCustomer?.customer_code) { setPriceItems([]); setPriceCheckedIds([]); return; } + setPriceCheckedIds([]); const fetchItems = async () => { setPriceLoading(true); try { @@ -345,7 +348,8 @@ export default function CustomerManagementPage() { // 납품처 조회 useEffect(() => { - if (!selectedCustomer?.customer_code) { setDeliveryItems([]); return; } + if (!selectedCustomer?.customer_code) { setDeliveryItems([]); setDeliveryCheckedIds([]); return; } + setDeliveryCheckedIds([]); const fetchDelivery = async () => { setDeliveryLoading(true); try { @@ -786,6 +790,69 @@ export default function CustomerManagementPage() { } }; + // 우측: 품목 매핑 삭제 (관련 단가도 함께 삭제) + const handlePriceItemDelete = async () => { + if (priceCheckedIds.length === 0) return; + const ok = await confirm(`선택한 ${priceCheckedIds.length}개 품목 매핑을 삭제하시겠습니까?`, { + description: "관련된 단가 정보도 함께 삭제됩니다.", + variant: "destructive", confirmText: "삭제", + }); + if (!ok) return; + try { + // 매핑 삭제 — 관련 단가는 서버에서 cascade 또는 별도 삭제 + // 먼저 관련 단가 삭제 시도 + for (const mappingId of priceCheckedIds) { + try { + const priceRes = await apiClient.post(`/table-management/tables/${PRICE_TABLE}/data`, { + page: 1, size: 500, + dataFilter: { enabled: true, filters: [{ columnName: "mapping_id", operator: "equals", value: mappingId }] }, + autoFilter: true, + }); + const prices = priceRes.data?.data?.data || priceRes.data?.data?.rows || []; + if (prices.length > 0) { + await apiClient.delete(`/table-management/tables/${PRICE_TABLE}/delete`, { + data: prices.map((p: any) => ({ id: p.id })), + }); + } + } catch { /* skip */ } + } + // 매핑 삭제 + await apiClient.delete(`/table-management/tables/${MAPPING_TABLE}/delete`, { + data: priceCheckedIds.map((id) => ({ id })), + }); + toast.success(`${priceCheckedIds.length}개 품목 매핑이 삭제되었습니다.`); + setPriceCheckedIds([]); + // 우측 새로고침 + const cid = selectedCustomerId; + setSelectedCustomerId(null); + setTimeout(() => setSelectedCustomerId(cid), 50); + } catch { + toast.error("삭제에 실패했습니다."); + } + }; + + // 우측: 납품처 삭제 + const handleDeliveryDelete = async () => { + if (deliveryCheckedIds.length === 0) return; + const ok = await confirm(`선택한 ${deliveryCheckedIds.length}개 납품처를 삭제하시겠습니까?`, { + variant: "destructive", confirmText: "삭제", + }); + if (!ok) return; + try { + await apiClient.delete(`/table-management/tables/${DELIVERY_TABLE}/delete`, { + data: deliveryCheckedIds.map((id) => ({ id })), + }); + toast.success(`${deliveryCheckedIds.length}개 납품처가 삭제되었습니다.`); + setDeliveryCheckedIds([]); + // 우측 새로고침 + const cid = selectedCustomerId; + setSelectedCustomerId(null); + setTimeout(() => setSelectedCustomerId(cid), 50); + } catch { + toast.error("삭제에 실패했습니다."); + } + }; + // 엑셀 다운로드 const handleExcelDownload = async () => { if (customers.length === 0) return; @@ -967,16 +1034,28 @@ export default function CustomerManagementPage() {
{rightTab === "items" && ( - + <> + + + )} {rightTab === "delivery" && ( - + <> + + + )}
@@ -993,6 +1072,9 @@ export default function CustomerManagementPage() { data={priceItems} loading={priceLoading} showRowNumber={false} + showCheckbox + checkedIds={priceCheckedIds} + onCheckedChange={setPriceCheckedIds} tableName={MAPPING_TABLE} emptyMessage="등록된 품목이 없습니다" onRowDoubleClick={(row) => openEditItem(row)} @@ -1012,6 +1094,9 @@ export default function CustomerManagementPage() { data={deliveryItems} loading={deliveryLoading} showRowNumber={false} + showCheckbox + checkedIds={deliveryCheckedIds} + onCheckedChange={setDeliveryCheckedIds} tableName={DELIVERY_TABLE} emptyMessage="등록된 납품처가 없습니다" /> diff --git a/frontend/app/(main)/COMPANY_7/sales/order/page.tsx b/frontend/app/(main)/COMPANY_7/sales/order/page.tsx index 0b889292..939c9787 100644 --- a/frontend/app/(main)/COMPANY_7/sales/order/page.tsx +++ b/frontend/app/(main)/COMPANY_7/sales/order/page.tsx @@ -209,6 +209,9 @@ export default function SalesOrderPage() { } catch { /* skip */ } } setCategoryOptions(optMap); + // division 기본값: 라벨이 "영업관리"인 코드로 설정 + const salesDiv = (optMap["item_division"] || []).find((o: any) => o.label === "영업관리"); + if (salesDiv) setItemSearchDivision(salesDiv.code); }; loadCategories(); }, []); diff --git a/frontend/app/(main)/COMPANY_7/sales/sales-item/page.tsx b/frontend/app/(main)/COMPANY_7/sales/sales-item/page.tsx index a2fcd9a9..7847fa55 100644 --- a/frontend/app/(main)/COMPANY_7/sales/sales-item/page.tsx +++ b/frontend/app/(main)/COMPANY_7/sales/sales-item/page.tsx @@ -79,6 +79,7 @@ export default function SalesItemPage() { // 우측: 거래처 const [customerItems, setCustomerItems] = useState([]); const [customerLoading, setCustomerLoading] = useState(false); + const [customerCheckedIds, setCustomerCheckedIds] = useState([]); // 카테고리 const [categoryOptions, setCategoryOptions] = useState>({}); @@ -200,12 +201,13 @@ export default function SalesItemPage() { // 우측: 거래처 목록 조회 useEffect(() => { - if (!selectedItem?.item_number) { setCustomerItems([]); return; } + if (!selectedItem?.item_number) { setCustomerItems([]); setCustomerCheckedIds([]); return; } + setCustomerCheckedIds([]); const itemKey = selectedItem.item_number; const fetchCustomerItems = async () => { setCustomerLoading(true); try { - // customer_item_mapping에서 해당 품목의 매핑 조회 + // 1. customer_item_mapping에서 해당 품목의 매핑 조회 const mapRes = await apiClient.post(`/table-management/tables/${MAPPING_TABLE}/data`, { page: 1, size: 500, dataFilter: { enabled: true, filters: [{ columnName: "item_id", operator: "equals", value: itemKey }] }, @@ -213,7 +215,7 @@ export default function SalesItemPage() { }); const mappings = mapRes.data?.data?.data || mapRes.data?.data?.rows || []; - // customer_id → customer_mng 조인 (거래처명) + // 2. customer_id → customer_mng 조인 (거래처명) const custIds = [...new Set(mappings.map((m: any) => m.customer_id).filter(Boolean))]; let custMap: Record = {}; if (custIds.length > 0) { @@ -229,11 +231,54 @@ export default function SalesItemPage() { } catch { /* skip */ } } - setCustomerItems(mappings.map((m: any) => ({ - ...m, - customer_code: m.customer_id, - customer_name: custMap[m.customer_id]?.customer_name || "", - }))); + // 3. customer_item_prices 조회 (단가 정보) + let allPrices: any[] = []; + if (mappings.length > 0) { + try { + const priceRes = await apiClient.post(`/table-management/tables/customer_item_prices/data`, { + page: 1, size: 500, + dataFilter: { enabled: true, filters: [ + { columnName: "item_id", operator: "equals", value: itemKey }, + ]}, + autoFilter: true, + }); + allPrices = priceRes.data?.data?.data || priceRes.data?.data?.rows || []; + } catch { /* skip */ } + } + + // 4. 거래처별 중복 제거 + 오늘 날짜 기준 단가 매칭 + const priceResolve = (col: string, code: string) => { + if (!code) return ""; + return priceCategoryOptions[col]?.find((o: any) => o.code === code)?.label || code; + }; + const today = new Date().toISOString().split("T")[0]; + const seenCustIds = new Set(); + + // customer_id로 정렬하여 같은 거래처끼리 묶기 + const sortedMappings = [...mappings].sort((a: any, b: any) => (a.customer_id || "").localeCompare(b.customer_id || "")); + + setCustomerItems(sortedMappings.map((m: any) => { + const custKey = m.customer_id || ""; + const isFirstOfGroup = !seenCustIds.has(custKey); + if (custKey) seenCustIds.add(custKey); + + // 오늘 날짜에 해당하는 단가 + const custPriceList = allPrices.filter((p: any) => p.customer_id === custKey); + const todayPrice = custPriceList.find((p: any) => + (!p.start_date || p.start_date <= today) && (!p.end_date || p.end_date >= today) + ) || custPriceList[0] || {}; + + return { + ...m, + customer_code: isFirstOfGroup ? custKey : "", + customer_name: isFirstOfGroup ? (custMap[custKey]?.customer_name || "") : "", + customer_item_code: m.customer_item_code || "", + customer_item_name: m.customer_item_name || "", + base_price: todayPrice.base_price || "", + calculated_price: todayPrice.calculated_price || todayPrice.unit_price || "", + currency_code: priceResolve("currency_code", todayPrice.currency_code || ""), + }; + })); } catch (err) { console.error("거래처 조회 실패:", err); } finally { @@ -241,7 +286,7 @@ export default function SalesItemPage() { } }; fetchCustomerItems(); - }, [selectedItem?.item_number]); + }, [selectedItem?.item_number, priceCategoryOptions]); // 거래처 검색 const searchCustomers = async () => { @@ -523,6 +568,46 @@ export default function SalesItemPage() { } }; + // 우측: 거래처 매핑 삭제 + const handleCustomerMappingDelete = async () => { + if (customerCheckedIds.length === 0) return; + const ok = await confirm(`선택한 ${customerCheckedIds.length}개 거래처 매핑을 삭제하시겠습니까?`, { + description: "관련된 단가 정보도 함께 삭제됩니다.", + variant: "destructive", confirmText: "삭제", + }); + if (!ok) return; + try { + // 관련 단가 삭제 + for (const mappingId of customerCheckedIds) { + try { + const priceRes = await apiClient.post(`/table-management/tables/customer_item_prices/data`, { + page: 1, size: 500, + dataFilter: { enabled: true, filters: [{ columnName: "mapping_id", operator: "equals", value: mappingId }] }, + autoFilter: true, + }); + const prices = priceRes.data?.data?.data || priceRes.data?.data?.rows || []; + if (prices.length > 0) { + await apiClient.delete(`/table-management/tables/customer_item_prices/delete`, { + data: prices.map((p: any) => ({ id: p.id })), + }); + } + } catch { /* skip */ } + } + // 매핑 삭제 + await apiClient.delete(`/table-management/tables/${MAPPING_TABLE}/delete`, { + data: customerCheckedIds.map((id) => ({ id })), + }); + toast.success(`${customerCheckedIds.length}개 거래처 매핑이 삭제되었습니다.`); + setCustomerCheckedIds([]); + // 우측 새로고침 + const sid = selectedItemId; + setSelectedItemId(null); + setTimeout(() => setSelectedItemId(sid), 50); + } catch { + toast.error("삭제에 실패했습니다."); + } + }; + // 엑셀 다운로드 const handleExcelDownload = async () => { if (items.length === 0) return; @@ -598,10 +683,16 @@ export default function SalesItemPage() { 거래처 정보 {selectedItem && {selectedItem.item_name}} - +
+ + +
{!selectedItemId ? (
@@ -614,6 +705,9 @@ export default function SalesItemPage() { data={customerItems} loading={customerLoading} showRowNumber={false} + showCheckbox + checkedIds={customerCheckedIds} + onCheckedChange={setCustomerCheckedIds} emptyMessage="등록된 거래처가 없습니다" onRowDoubleClick={(row) => openEditCust(row)} /> diff --git a/frontend/app/(main)/COMPANY_7/sales/shipping-order/page.tsx b/frontend/app/(main)/COMPANY_7/sales/shipping-order/page.tsx index 5ff26159..25e52ead 100644 --- a/frontend/app/(main)/COMPANY_7/sales/shipping-order/page.tsx +++ b/frontend/app/(main)/COMPANY_7/sales/shipping-order/page.tsx @@ -76,6 +76,7 @@ export default function ShippingOrderPage() { const [orders, setOrders] = useState([]); const [loading, setLoading] = useState(false); const [checkedIds, setCheckedIds] = useState([]); + const [selectedOrderId, setSelectedOrderId] = useState(null); // 검색 const [searchKeyword, setSearchKeyword] = useState(""); @@ -526,7 +527,7 @@ export default function ShippingOrderPage() { const items = Array.isArray(order.items) ? order.items.filter((it: any) => it.id) : []; if (items.length === 0) { return ( - openModal(order)}> + setSelectedOrderId(order.id)} onDoubleClick={() => openModal(order)}> e.stopPropagation()}> { if (c) setCheckedIds(p => [...p, order.id]); @@ -551,7 +552,7 @@ export default function ShippingOrderPage() { ); } return items.map((item: any, itemIdx: number) => ( - openModal(order)}> + setSelectedOrderId(order.id)} onDoubleClick={() => openModal(order)}> e.stopPropagation()}> {itemIdx === 0 && { if (c) setCheckedIds(p => [...p, order.id]); diff --git a/frontend/components/common/DynamicSearchFilter.tsx b/frontend/components/common/DynamicSearchFilter.tsx index e61c7c5e..1a0f8acd 100644 --- a/frontend/components/common/DynamicSearchFilter.tsx +++ b/frontend/components/common/DynamicSearchFilter.tsx @@ -175,6 +175,13 @@ export function DynamicSearchFilter({ width: f.width, })); setActiveFilters(active); + // allColumns도 동기화하여 설정 모달에서 일관된 상태 표시 + setAllColumns((prev) => + prev.map((col) => { + const ext = externalFilterConfig.find((f) => f.columnName === col.columnName); + return ext ? { ...col, enabled: ext.enabled, filterType: ext.filterType, width: ext.width } : col; + }) + ); }, [externalFilterConfig]); // select 타입 필터의 옵션 로드 diff --git a/frontend/lib/api/receiving.ts b/frontend/lib/api/receiving.ts index f890609d..d9c97002 100644 --- a/frontend/lib/api/receiving.ts +++ b/frontend/lib/api/receiving.ts @@ -162,6 +162,7 @@ interface SourceParams { keyword?: string; page?: number; pageSize?: number; + division?: string; } export async function getPurchaseOrderSources(params?: SourceParams) { 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 2324f5f3..ff120d7d 100644 --- a/frontend/lib/registry/components/v2-bom-item-editor/BomItemEditorComponent.tsx +++ b/frontend/lib/registry/components/v2-bom-item-editor/BomItemEditorComponent.tsx @@ -320,11 +320,18 @@ function TreeNodeRow({ const renderCell = (col: BomColumnConfig) => { const value = node.data[col.key] ?? ""; - // 소스 표시 컬럼 (읽기 전용) + // 소스 표시 컬럼 (읽기 전용) — 카테고리 코드인 경우 라벨로 변환 if (col.isSourceDisplay) { + let displayValue = value; + if (col.inputType === "category" && mainTableName) { + const categoryRef = `${mainTableName}.${col.key}`; + const options = categoryOptionsMap[categoryRef] || []; + const found = options.find((opt) => opt.value === String(value)); + if (found) displayValue = found.label; + } return ( - - {value || "-"} + + {displayValue || "-"} ); } @@ -352,11 +359,18 @@ function TreeNodeRow({ ); } - // 편집 불가능 컬럼 + // 편집 불가능 컬럼 — 카테고리 코드인 경우 라벨로 변환 if (col.editable === false) { + let displayValue = value; + if (col.inputType === "category" && mainTableName) { + const categoryRef = `${mainTableName}.${col.key}`; + const options = categoryOptionsMap[categoryRef] || []; + const found = options.find((opt) => opt.value === String(value)); + if (found) displayValue = found.label; + } return ( - {value || "-"} + {displayValue || "-"} ); } diff --git a/frontend/lib/registry/components/v2-bom-tree/BomTreeComponent.tsx b/frontend/lib/registry/components/v2-bom-tree/BomTreeComponent.tsx index cf097527..f995f34a 100644 --- a/frontend/lib/registry/components/v2-bom-tree/BomTreeComponent.tsx +++ b/frontend/lib/registry/components/v2-bom-tree/BomTreeComponent.tsx @@ -43,6 +43,7 @@ interface TreeColumnDef { visible?: boolean; hidden?: boolean; isSourceDisplay?: boolean; + inputType?: string; } interface BomTreeComponentProps { @@ -141,22 +142,27 @@ export function BomTreeComponent({ const showHistory = features.showHistory !== false; const showVersion = features.showVersion !== false; - // 카테고리 라벨 캐시 (process_type 등) + // 카테고리 라벨 캐시 (inputType === "category"인 모든 컬럼) const [categoryLabels, setCategoryLabels] = useState>>({}); useEffect(() => { + const categoryColumns = displayColumns.filter((c) => c.inputType === "category"); + if (categoryColumns.length === 0) return; + const loadLabels = async () => { - try { - const res = await apiClient.get(`/table-categories/${detailTable}/process_type/values?includeInactive=true`); - 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 { /* 무시 */ } + for (const col of categoryColumns) { + try { + const res = await apiClient.get(`/table-categories/${detailTable}/${col.key}/values?includeInactive=true`); + 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, [col.key]: map })); + } + } catch { /* 무시 */ } + } }; loadLabels(); - }, [detailTable]); + }, [detailTable, displayColumns]); // ─── 데이터 로드 ─── @@ -518,9 +524,10 @@ export function BomTreeComponent({ ); } - if (col.key === "process_type" && value) { - const label = categoryLabels.process_type?.[String(value)] || String(value); - return {label}; + // 카테고리 타입 컬럼: 코드 → 라벨 변환 + if (col.inputType === "category" && categoryLabels[col.key]) { + const label = categoryLabels[col.key][String(value)] || String(value || ""); + return {label || "-"}; } if (col.key === "loss_rate") { @@ -538,7 +545,14 @@ export function BomTreeComponent({ } if (col.key === "unit") { - return {value || "-"}; + const unitLabel = categoryLabels[col.key]?.[String(value)] || value; + return {unitLabel || "-"}; + } + + // fallback: 카테고리 라벨이 로드된 컬럼이면 라벨로 변환 + if (categoryLabels[col.key] && value) { + const label = categoryLabels[col.key][String(value)] || String(value); + return {label || "-"}; } return {value ?? "-"}; @@ -577,7 +591,7 @@ export function BomTreeComponent({ }; return ( -
+
{/* 헤더 (실제 화면과 동일 구조) */}
@@ -770,7 +784,7 @@ export function BomTreeComponent({ // ─── 메인 렌더링 ─── return ( -
+
{/* 헤더 정보 */} {features.showHeader !== false && headerInfo && (
@@ -959,11 +973,11 @@ export function BomTreeComponent({ const displayDepth = isRoot ? 0 : depth; const lvlDepthBg = isRoot - ? "border-border bg-primary/10/50 font-medium hover:bg-primary/10/70" + ? "border-border bg-primary/10 font-medium hover:bg-primary/20" : selectedNodeId === node.id ? "border-border bg-primary/5" : depth === 1 - ? "border-border bg-white hover:bg-muted/60" + ? "border-border bg-background hover:bg-muted/60" : depth === 2 ? "border-border bg-muted/40 hover:bg-muted/50" : depth >= 3 @@ -1053,11 +1067,11 @@ export function BomTreeComponent({ const ItemIcon = getItemIcon(itemType); const depthBg = isRoot - ? "border-border bg-primary/10/50 font-medium hover:bg-primary/10/70" + ? "border-border bg-primary/10 font-medium hover:bg-primary/20" : isSelected ? "border-border bg-primary/5" : depth === 1 - ? "border-border bg-white hover:bg-muted/60" + ? "border-border bg-background hover:bg-muted/60" : depth === 2 ? "border-border bg-muted/40 hover:bg-muted/50" : depth >= 3 diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 13f76fcf..8c84a50b 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1418,7 +1418,6 @@ "integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "playwright": "1.58.2" },