diff --git a/src/components/production/WorkOrders/documents/BendingInspectionContent.tsx b/src/components/production/WorkOrders/documents/BendingInspectionContent.tsx index 0e640696..9de839f8 100644 --- a/src/components/production/WorkOrders/documents/BendingInspectionContent.tsx +++ b/src/components/production/WorkOrders/documents/BendingInspectionContent.tsx @@ -16,6 +16,7 @@ import { useState, useCallback, useMemo, forwardRef, useImperativeHandle, useEffect } from 'react'; import type { WorkOrder } from '../types'; +import type { BendingInfoExtended } from './bending/types'; import type { InspectionData } from '@/components/production/WorkerScreen/InspectionInputModal'; import type { WorkItemData } from '@/components/production/WorkerScreen/types'; import type { InspectionDataMap } from './InspectionReportModal'; @@ -68,71 +69,140 @@ interface ProductRow { gapPoints: GapPoint[]; } -const INITIAL_PRODUCTS: Omit[] = [ - { - id: 'guide-rail-wall', category: 'KWE01', productName: '가이드레일', productType: '벽면형', - lengthDesign: '3000', widthDesign: 'N/A', - gapPoints: [ - { point: '①', designValue: '30', measured: '' }, - { point: '②', designValue: '80', measured: '' }, - { point: '③', designValue: '45', measured: '' }, - { point: '④', designValue: '40', measured: '' }, - { point: '⑤', designValue: '34', measured: '' }, - ], - }, - { - id: 'guide-rail-side', category: 'KWE01', productName: '가이드레일', productType: '측면형', - lengthDesign: '3000', widthDesign: 'N/A', - gapPoints: [ - { point: '①', designValue: '28', measured: '' }, - { point: '②', designValue: '75', measured: '' }, - { point: '③', designValue: '42', measured: '' }, - { point: '④', designValue: '38', measured: '' }, - { point: '⑤', designValue: '32', measured: '' }, - ], - }, - { - id: 'case', category: 'KWE01', productName: '케이스', productType: '500X380', - lengthDesign: '3000', widthDesign: 'N/A', - gapPoints: [ - { point: '①', designValue: '380', measured: '' }, - { point: '②', designValue: '50', measured: '' }, - { point: '③', designValue: '240', measured: '' }, - { point: '④', designValue: '50', measured: '' }, - ], - }, - { - id: 'bottom-finish', category: 'KWE01', productName: '하단마감재', productType: '60X40', - lengthDesign: '3000', widthDesign: 'N/A', - gapPoints: [ - { point: '②', designValue: '60', measured: '' }, - { point: '②', designValue: '64', measured: '' }, - ], - }, - { - id: 'bottom-l-bar', category: 'KWE01', productName: '하단L-BAR', productType: '17X60', - lengthDesign: '3000', widthDesign: 'N/A', - gapPoints: [ - { point: '①', designValue: '17', measured: '' }, - ], - }, - { - id: 'smoke-w50', category: 'KWE01', productName: '연기차단재', productType: 'W50\n가이드레일용', - lengthDesign: '3000', widthDesign: '', - gapPoints: [ - { point: '①', designValue: '50', measured: '' }, - { point: '②', designValue: '12', measured: '' }, - ], - }, - { - id: 'smoke-w80', category: 'KWE01', productName: '연기차단재', productType: 'W80\n케이스용', - lengthDesign: '3000', widthDesign: '', - gapPoints: [ - { point: '①', designValue: '80', measured: '' }, - { point: '②', designValue: '12', measured: '' }, - ], - }, -]; +/** 기본 gap 프로파일 (bendingInfo 없을 때 폴백) */ +const DEFAULT_GAP_PROFILES = { + wall: [ + { point: '①', designValue: '30' }, { point: '②', designValue: '80' }, + { point: '③', designValue: '45' }, { point: '④', designValue: '40' }, + { point: '⑤', designValue: '34' }, + ], + side: [ + { point: '①', designValue: '28' }, { point: '②', designValue: '75' }, + { point: '③', designValue: '42' }, { point: '④', designValue: '38' }, + { point: '⑤', designValue: '32' }, + ], + case: [ + { point: '①', designValue: '380' }, { point: '②', designValue: '50' }, + { point: '③', designValue: '240' }, { point: '④', designValue: '50' }, + ], + bottomBar: [ + { point: '①', designValue: '60' }, { point: '②', designValue: '64' }, + ], + lBar: [{ point: '①', designValue: '17' }], + smokeW50: [{ point: '①', designValue: '50' }, { point: '②', designValue: '12' }], + smokeW80: [{ point: '①', designValue: '80' }, { point: '②', designValue: '12' }], +}; + +type ProductTemplate = Omit; + +/** bendingInfo 기반으로 검사 대상 부품 동적 생성 */ +function buildProductsFromBendingInfo(bendingInfo?: BendingInfoExtended): ProductTemplate[] { + if (!bendingInfo?.productCode) return []; + const code = bendingInfo.productCode; + const isStock = bendingInfo.isStockProduction; + const stockKeys = new Set(bendingInfo.stockPartFilter?.map(f => f.partKey) || []); + + // 길이 추출 헬퍼 + const getLength = (data?: { lengthData?: Array<{ length: number }> }): string => { + const len = data?.lengthData?.[0]?.length; + return len ? String(len) : '3000'; + }; + + const products: ProductTemplate[] = []; + + // 가이드레일 벽면 + const hasWall = bendingInfo?.guideRail?.wall; + if (hasWall && (!isStock || stockKeys.size === 0)) { + products.push({ + id: 'guide-rail-wall', category: code, productName: '가이드레일', productType: '벽면형', + lengthDesign: getLength(hasWall), widthDesign: 'N/A', + gapPoints: DEFAULT_GAP_PROFILES.wall.map(p => ({ ...p, measured: '' })), + }); + } + + // 가이드레일 측면 + const hasSide = bendingInfo?.guideRail?.side; + if (hasSide) { + // STOCK 필터: 가이드레일 관련 부품이면 표시 + if (!isStock || stockKeys.size === 0 || ['본체', 'C형', 'D형', '마감재', '별도마감'].some(k => stockKeys.has(k))) { + products.push({ + id: 'guide-rail-side', category: code, productName: '가이드레일', productType: '측면형', + lengthDesign: getLength(hasSide), widthDesign: 'N/A', + gapPoints: DEFAULT_GAP_PROFILES.side.map(p => ({ ...p, measured: '' })), + }); + } + } + + // 케이스 + if (bendingInfo?.shutterBox && bendingInfo.shutterBox.length > 0) { + const box = bendingInfo.shutterBox[0]; + products.push({ + id: 'case', category: code, productName: '케이스', productType: box.size || '500X380', + lengthDesign: getLength(box), widthDesign: 'N/A', + gapPoints: DEFAULT_GAP_PROFILES.case.map(p => ({ ...p, measured: '' })), + }); + } else if (!isStock) { + products.push({ + id: 'case', category: code, productName: '케이스', productType: '500X380', + lengthDesign: '3000', widthDesign: 'N/A', + gapPoints: DEFAULT_GAP_PROFILES.case.map(p => ({ ...p, measured: '' })), + }); + } + + // 하단마감재 + if (bendingInfo?.bottomBar) { + const len = bendingInfo.bottomBar.length3000Qty > 0 ? '3000' : bendingInfo.bottomBar.length4000Qty > 0 ? '4000' : '3000'; + products.push({ + id: 'bottom-finish', category: code, productName: '하단마감재', productType: '60X40', + lengthDesign: len, widthDesign: 'N/A', + gapPoints: DEFAULT_GAP_PROFILES.bottomBar.map(p => ({ ...p, measured: '' })), + }); + products.push({ + id: 'bottom-l-bar', category: code, productName: '하단L-BAR', productType: '17X60', + lengthDesign: len, widthDesign: 'N/A', + gapPoints: DEFAULT_GAP_PROFILES.lBar.map(p => ({ ...p, measured: '' })), + }); + } else if (!isStock) { + products.push({ + id: 'bottom-finish', category: code, productName: '하단마감재', productType: '60X40', + lengthDesign: '3000', widthDesign: 'N/A', + gapPoints: DEFAULT_GAP_PROFILES.bottomBar.map(p => ({ ...p, measured: '' })), + }); + products.push({ + id: 'bottom-l-bar', category: code, productName: '하단L-BAR', productType: '17X60', + lengthDesign: '3000', widthDesign: 'N/A', + gapPoints: DEFAULT_GAP_PROFILES.lBar.map(p => ({ ...p, measured: '' })), + }); + } + + // 연기차단재 + if (bendingInfo?.smokeBarrier) { + const len = bendingInfo.smokeBarrier.w50?.[0]?.length ? String(bendingInfo.smokeBarrier.w50[0].length) : '3000'; + products.push({ + id: 'smoke-w50', category: code, productName: '연기차단재', productType: 'W50\n가이드레일용', + lengthDesign: len, widthDesign: '', + gapPoints: DEFAULT_GAP_PROFILES.smokeW50.map(p => ({ ...p, measured: '' })), + }); + products.push({ + id: 'smoke-w80', category: code, productName: '연기차단재', productType: 'W80\n케이스용', + lengthDesign: len, widthDesign: '', + gapPoints: DEFAULT_GAP_PROFILES.smokeW80.map(p => ({ ...p, measured: '' })), + }); + } else if (!isStock) { + products.push({ + id: 'smoke-w50', category: code, productName: '연기차단재', productType: 'W50\n가이드레일용', + lengthDesign: '3000', widthDesign: '', + gapPoints: DEFAULT_GAP_PROFILES.smokeW50.map(p => ({ ...p, measured: '' })), + }); + products.push({ + id: 'smoke-w80', category: code, productName: '연기차단재', productType: 'W80\n케이스용', + lengthDesign: '3000', widthDesign: '', + gapPoints: DEFAULT_GAP_PROFILES.smokeW80.map(p => ({ ...p, measured: '' })), + }); + } + + return products; +} export const BendingInspectionContent = forwardRef(function BendingInspectionContent({ data: order, @@ -147,34 +217,32 @@ export const BendingInspectionContent = forwardRef(() => - INITIAL_PRODUCTS.map(p => ({ - ...p, - bendingStatus: null, - lengthMeasured: '', - widthMeasured: '', - gapPoints: p.gapPoints.map(gp => ({ ...gp })), - })) - ); + const bendingInfo = order.bendingInfo as BendingInfoExtended | undefined; + const productCode = bendingInfo?.productCode || '-'; + const finishMaterial = bendingInfo?.finishMaterial || '-'; + const [products, setProducts] = useState([]); const [inadequateContent, setInadequateContent] = useState(''); + // bendingInfo + 저장된 검사 데이터를 통합하여 products 생성 useEffect(() => { - if (!workItems || workItems.length === 0 || !inspectionDataMap) return; + // 1. bendingInfo 기반 templates 생성 (해당 부품만) + const templates = buildProductsFromBendingInfo(bendingInfo); + if (templates.length === 0) return; - // 검사 데이터가 있는 첫 번째 workItem 찾기 (workItems[0]에만 의존하지 않음) + // 2. 저장된 검사 데이터 찾기 let itemData: InspectionData | undefined; - for (const item of workItems) { - const data = inspectionDataMap.get(item.id); - if (data) { - itemData = data as unknown as InspectionData; - break; + if (workItems && inspectionDataMap) { + for (const item of workItems) { + const data = inspectionDataMap.get(item.id); + if (data) { + itemData = data as unknown as InspectionData; + break; + } } } - if (!itemData) return; - // 저장된 검사 성적서 포맷 (products 배열) 복원 - const savedProducts = (itemData as unknown as Record).products as Array<{ + const savedProducts = (itemData as unknown as Record | undefined)?.products as Array<{ id: string; bendingStatus: CheckStatus; lengthMeasured: string; @@ -182,53 +250,35 @@ export const BendingInspectionContent = forwardRef; }> | undefined; - if (savedProducts && Array.isArray(savedProducts)) { - // 검사 성적서에서 저장된 전체 데이터 복원 - setProducts(prev => prev.map(p => { - const saved = savedProducts.find(sp => sp.id === p.id); - if (!saved) return p; - return { - ...p, - bendingStatus: saved.bendingStatus ?? p.bendingStatus, - lengthMeasured: saved.lengthMeasured || p.lengthMeasured, - widthMeasured: saved.widthMeasured || p.widthMeasured, - gapPoints: p.gapPoints.map((gp, gi) => ({ - ...gp, - measured: saved.gapPoints?.[gi]?.measured || gp.measured, - })), - }; - })); - // 부적합 내용 복원 - const savedInadequate = (itemData as unknown as Record).inadequateContent; - if (typeof savedInadequate === 'string' && savedInadequate) { - setInadequateContent(savedInadequate); - } - return; - } + // 3. templates 기반으로 products 생성 + 저장 데이터 병합 + const newProducts: ProductRow[] = templates.map(p => { + const saved = savedProducts?.find(sp => sp.id === p.id); + const inferredStatus: CheckStatus = itemData?.bendingStatus + ? convertToCheckStatus(itemData.bendingStatus) + : itemData?.judgment === 'pass' ? '양호' : itemData?.judgment === 'fail' ? '불량' : null; - // 개소별 검사 입력 데이터에서 bendingStatus 로드 - if (itemData.bendingStatus) { - const bendingStatusValue = convertToCheckStatus(itemData.bendingStatus); - setProducts(prev => prev.map(p => ({ + return { ...p, - bendingStatus: bendingStatusValue, - }))); - } else if (itemData.judgment) { - // 이전 형식 호환: products/bendingStatus 없이 judgment만 있는 경우 - const inferredStatus: CheckStatus = itemData.judgment === 'pass' ? '양호' : itemData.judgment === 'fail' ? '불량' : null; - if (inferredStatus) { - setProducts(prev => prev.map(p => ({ - ...p, - bendingStatus: inferredStatus, - }))); - } - } + bendingStatus: saved?.bendingStatus ?? inferredStatus ?? null, + lengthMeasured: saved?.lengthMeasured || '', + widthMeasured: saved?.widthMeasured || '', + gapPoints: p.gapPoints.map((gp, gi) => ({ + ...gp, + measured: saved?.gapPoints?.[gi]?.measured || '', + })), + }; + }); - // 부적합 내용 로드 - if (itemData.nonConformingContent) { + setProducts(newProducts); + + // 부적합 내용 복원 + const savedInadequate = (itemData as unknown as Record | undefined)?.inadequateContent; + if (typeof savedInadequate === 'string' && savedInadequate) { + setInadequateContent(savedInadequate); + } else if (itemData?.nonConformingContent) { setInadequateContent(itemData.nonConformingContent); } - }, [workItems, inspectionDataMap]); + }, [bendingInfo?.productCode, bendingInfo?.isStockProduction, workItems, inspectionDataMap]); const handleStatusChange = useCallback((productId: string, value: CheckStatus) => { if (readOnly) return; @@ -291,7 +341,7 @@ export const BendingInspectionContent = forwardRef 제품명 - 슬랫 + {productCode} 제품 LOT NO {order.lotNo || '-'} @@ -315,9 +365,9 @@ export const BendingInspectionContent = forwardRef 제품명 - KWE01 + {productCode} 마감유형 - 소니자감 + {finishMaterial || '-'} diff --git a/src/components/production/WorkOrders/documents/BendingWipInspectionContent.tsx b/src/components/production/WorkOrders/documents/BendingWipInspectionContent.tsx index 09f15203..97029b3d 100644 --- a/src/components/production/WorkOrders/documents/BendingWipInspectionContent.tsx +++ b/src/components/production/WorkOrders/documents/BendingWipInspectionContent.tsx @@ -14,6 +14,7 @@ import { useState, useCallback, useMemo, forwardRef, useImperativeHandle, useEffect } from 'react'; import type { WorkOrder } from '../types'; +import type { BendingInfoExtended } from './bending/types'; import type { InspectionData } from '@/components/production/WorkerScreen/InspectionInputModal'; import type { WorkItemData } from '@/components/production/WorkerScreen/types'; import type { InspectionDataMap } from './InspectionReportModal'; @@ -51,6 +52,7 @@ export interface BendingWipInspectionContentProps { interface InspectionRow { id: number; itemId?: string; + partName: string; // 검사부위: "가이드레일(측면) 본체(철재)" productName: string; processStatus: CheckStatus; lengthDesign: string; @@ -62,21 +64,42 @@ interface InspectionRow { spacingMeasured: string; } -function buildRow(i: number, order: WorkOrder, workItems?: WorkItemData[], inspectionDataMap?: InspectionDataMap): InspectionRow { +/** 품목명에서 길이(mm) 추출: "가이드레일(측면) 본체(철재) 2438mm" → "2438" */ +function extractLengthFromItemName(name?: string): string { + if (!name) return '-'; + const m = name.match(/(\d{3,5})\s*mm/i); + return m ? m[1] : '-'; +} + +function buildRow(i: number, order: WorkOrder, workItems?: WorkItemData[], inspectionDataMap?: InspectionDataMap, bendingInfo?: BendingInfoExtended): InspectionRow { const item = workItems?.[i]; const orderItem = order.items?.[i]; const itemData = item && inspectionDataMap?.get(item.id); + const itemName = item?.itemName || orderItem?.productName || ''; + + // 실제 길이: 품목명에서 추출 → bendingInfo에서 추출 → 폴백 + let lengthDesign = extractLengthFromItemName(itemName); + if (lengthDesign === '-' && bendingInfo?.guideRail) { + const data = bendingInfo.guideRail.side || bendingInfo.guideRail.wall; + const len = data?.lengthData?.[0]?.length; + if (len) lengthDesign = String(len); + } + + // 검사부위: 품목명에서 길이(mm) 부분 제거 → "가이드레일(측면) 본체(철재)" + const partName = itemName.replace(/\s*\d{3,5}\s*mm\s*/i, '').trim(); + return { id: i + 1, itemId: item?.id, - productName: item?.itemName || orderItem?.productName || '', + partName, + productName: itemName, processStatus: itemData ? convertToCheckStatus(itemData.bendingStatus) : null, - lengthDesign: '4000', + lengthDesign, lengthMeasured: '', widthDesign: 'N/A', widthMeasured: 'N/A', spacingPoint: '', - spacingDesign: '380', + spacingDesign: '-', spacingMeasured: '', }; } @@ -94,18 +117,19 @@ export const BendingWipInspectionContent = forwardRef(() => - Array.from({ length: rowCount }, (_, i) => buildRow(i, order, workItems, inspectionDataMap)) - ); + // 실제 품목 수만큼만 행 생성 (빈 행 제거) + const actualItemCount = order.items?.filter(item => item.productName)?.length || 0; + const rowCount = actualItemCount || 1; + const [rows, setRows] = useState([]); const [inadequateContent, setInadequateContent] = useState(''); useEffect(() => { - const newRowCount = workItems?.length || order.items?.length || DEFAULT_ROW_COUNT; - setRows(Array.from({ length: newRowCount }, (_, i) => buildRow(i, order, workItems, inspectionDataMap))); - }, [workItems, inspectionDataMap, order.items]); + const count = order.items?.filter(item => item.productName)?.length || 1; + setRows(Array.from({ length: count }, (_, i) => buildRow(i, order, workItems, inspectionDataMap, bendingInfo))); + }, [workItems, inspectionDataMap, order.items, bendingInfo?.productCode]); const handleStatusChange = useCallback((rowId: number, value: CheckStatus) => { if (readOnly) return; @@ -172,7 +196,10 @@ export const BendingWipInspectionContent = forwardRef 길이 - 3,000 mm + {(() => { + const len = extractLengthFromItemName(order.items?.[0]?.productName); + return len !== '-' ? `${Number(len).toLocaleString()} mm` : '-'; + })()} 수량 {order.items?.reduce((sum, item) => sum + item.quantity, 0) || 0} EA @@ -243,7 +270,7 @@ export const BendingWipInspectionContent = forwardRef No. - 제품명 + 검사 부위 절곡상태
겉모양 길이 (mm) 너비 (mm) @@ -266,9 +293,7 @@ export const BendingWipInspectionContent = forwardRef {row.id} - - handleInputChange(row.id, 'productName', e.target.value)} disabled={readOnly} className={INPUT_CLASS} placeholder="-" /> - + {row.partName || '-'} handleStatusChange(row.id, v)} readOnly={readOnly} /> {row.lengthDesign} diff --git a/src/components/production/WorkOrders/documents/BendingWorkLogContent.tsx b/src/components/production/WorkOrders/documents/BendingWorkLogContent.tsx index ea74e6d5..188510bf 100644 --- a/src/components/production/WorkOrders/documents/BendingWorkLogContent.tsx +++ b/src/components/production/WorkOrders/documents/BendingWorkLogContent.tsx @@ -17,6 +17,7 @@ * - 생산량 합계 [kg] SUS/EGI */ +import { useMemo } from 'react'; import type { WorkOrder } from '../types'; import type { BendingInfoExtended } from './bending/types'; import { ConstructionApprovalTable } from '@/components/document-system'; @@ -31,9 +32,10 @@ interface BendingWorkLogContentProps { data: WorkOrder; lotNoMap?: Record; // BD-{prefix}-{lengthCode} → LOT NO bendingImages?: Record; // R2 presigned URL 맵 + rawMaterialLotNo?: string; // STOCK: 원자재 투입 LOT 번호 } -export function BendingWorkLogContent({ data: order, lotNoMap, bendingImages }: BendingWorkLogContentProps) { +export function BendingWorkLogContent({ data: order, lotNoMap, bendingImages, rawMaterialLotNo }: BendingWorkLogContentProps) { const today = new Date().toLocaleDateString('ko-KR', { year: 'numeric', month: '2-digit', @@ -74,6 +76,35 @@ export function BendingWorkLogContent({ data: order, lotNoMap, bendingImages }: ? calculateProductionSummary(bendingInfo!, mapping) : { susTotal: 0, egiTotal: 0, grandTotal: 0 }; + // STOCK 단일부품: 원자재 LOT를 모든 부품에 적용 + const effectiveLotNoMap = useMemo(() => { + if (!rawMaterialLotNo || !bendingInfo?.isStockProduction) return lotNoMap; + const map = { ...lotNoMap }; + // stockPartFilter의 각 부품에 원자재 LOT 번호 매핑 + const stockFilter = bendingInfo.stockPartFilter; + if (stockFilter) { + for (const part of stockFilter) { + // partKey별 lotPrefix 매핑 + const prefixMap: Record = { + '본체': ['ST', 'SM', 'RT', 'RM'], + 'C형': ['SC', 'RC'], + 'D형': ['SD', 'RD'], + '마감재': ['SS', 'SE', 'RS', 'RE'], + '별도마감': ['YY'], + 'BASE': ['XX'], + }; + const prefixes = prefixMap[part.partKey] || []; + for (const prefix of prefixes) { + // 와일드카드: BD-{prefix}-* 형태로 모든 길이에 매핑 + map[`BD-${prefix}-*`] = rawMaterialLotNo; + } + } + } + // 와일드카드 없이도 fallback으로 작동하도록 _raw 키 추가 + map['_rawMaterialLot'] = rawMaterialLotNo; + return map; + }, [lotNoMap, rawMaterialLotNo, bendingInfo]); + return (
{/* ===== 헤더 영역 ===== */} @@ -166,23 +197,23 @@ export function BendingWorkLogContent({ data: order, lotNoMap, bendingImages }: bendingInfo={bendingInfo!} mapping={mapping} lotNo={order.lotNo} - lotNoMap={lotNoMap} + lotNoMap={effectiveLotNoMap} bendingImages={bendingImages} /> diff --git a/src/components/production/WorkOrders/documents/TemplateInspectionContent.tsx b/src/components/production/WorkOrders/documents/TemplateInspectionContent.tsx index aa2a0707..bde48093 100644 --- a/src/components/production/WorkOrders/documents/TemplateInspectionContent.tsx +++ b/src/components/production/WorkOrders/documents/TemplateInspectionContent.tsx @@ -400,14 +400,20 @@ export const TemplateInspectionContent = forwardRef 품목명 > 기본값 + const actualWallLen = String(wallLen || itemLen || 3500); + const actualSideLen = String(sideLen || itemLen || 3000); + const actualLen = itemLen || '3000'; return inspectionConfig.items.map((item): BendingProduct => { // API id → 표시용 매핑 (이름, 타입, 치수) const displayMap: Record = { - guide_rail_wall: { name: '가이드레일', type: '벽면형', len: String(wallLen || 3500), wid: 'N/A' }, - guide_rail_side: { name: '가이드레일', type: '측면형', len: String(sideLen || 3000), wid: 'N/A' }, - bottom_bar: { name: '하단마감재', type: '60×40', len: '3000', wid: 'N/A' }, - case_box: { name: '케이스', type: '양면', len: '3000', wid: 'N/A' }, + guide_rail_wall: { name: '가이드레일', type: '벽면형', len: actualWallLen, wid: 'N/A' }, + guide_rail_side: { name: '가이드레일', type: '측면형', len: actualSideLen, wid: 'N/A' }, + bottom_bar: { name: '하단마감재', type: '60×40', len: actualLen, wid: 'N/A' }, + case_box: { name: '케이스', type: '양면', len: actualLen, wid: 'N/A' }, smoke_w50: { name: '연기차단재', type: '화이바 W50\n가이드레일용', len: '-', wid: '50' }, smoke_w80: { name: '연기차단재', type: '화이바 W80\n케이스용', len: '-', wid: '80' }, }; @@ -1348,8 +1354,8 @@ export const TemplateInspectionContent = forwardRef 0) { + const allowedKeys = new Set(stockFilter.map(f => f.partKey)); + const filterRows = (rows: typeof wallRows) => + rows.filter(row => { + if (allowedKeys.has('본체') && row.partName.includes('본체')) return true; + if (allowedKeys.has('C형') && row.partName.includes('C형')) return true; + if (allowedKeys.has('D형') && row.partName.includes('D형')) return true; + if (allowedKeys.has('마감재') && row.partName.includes('마감')) return true; + if (allowedKeys.has('별도마감') && row.partName.includes('별도마감')) return true; + if (allowedKeys.has('BASE') && row.partName.includes('BASE')) return true; + return false; + }); + wallRows = filterRows(wallRows); + sideRows = filterRows(sideRows); + } + if (wallRows.length === 0 && sideRows.length === 0) return null; return ( diff --git a/src/components/production/WorkOrders/documents/bending/types.ts b/src/components/production/WorkOrders/documents/bending/types.ts index f0dd4ed8..bfdaea51 100644 --- a/src/components/production/WorkOrders/documents/bending/types.ts +++ b/src/components/production/WorkOrders/documents/bending/types.ts @@ -66,6 +66,13 @@ export interface BendingInfoExtended { w50: LengthQuantity[]; // 레일용 W50 w80Qty: number; // 케이스용 W80 수량 }; + + // STOCK 재고생산 전용 + isStockProduction?: boolean; + stockPartFilter?: Array<{ + itemName: string; // "가이드레일(측면) 본체(철재) 2438mm" + partKey: string; // "본체", "C형", "D형", "마감재" 등 + }>; } // 재질 매핑 결과 diff --git a/src/components/production/WorkOrders/documents/bending/utils.ts b/src/components/production/WorkOrders/documents/bending/utils.ts index e948f55e..48e4ccf0 100644 --- a/src/components/production/WorkOrders/documents/bending/utils.ts +++ b/src/components/production/WorkOrders/documents/bending/utils.ts @@ -342,6 +342,7 @@ export function buildBottomBarRows( productCode?: string, ): BottomBarPartRow[] { const rows: BottomBarPartRow[] = []; + if (!bottomBar) return rows; const codePrefix = productCode?.replace(/\d+$/, '') || 'KSS'; const isSteel = codePrefix === 'KTE'; const lotPrefix = isSteel ? 'TS' : (mapping.bottomBarFinish.includes('SUS') ? 'BS' : 'BE'); @@ -488,9 +489,10 @@ export function buildSmokeBarrierRows( smokeBarrier: BendingInfoExtended['smokeBarrier'], ): SmokeBarrierPartRow[] { const rows: SmokeBarrierPartRow[] = []; + if (!smokeBarrier) return rows; // 레일용 W50 - for (const ld of smokeBarrier.w50) { + for (const ld of (smokeBarrier.w50 ?? [])) { if (ld.quantity <= 0) continue; const w = calcWeight('EGI 0.8T', SMOKE_BARRIER_WIDTH, ld.length); const code = getSLengthCode(ld.length, '연기차단재50'); @@ -503,7 +505,7 @@ export function buildSmokeBarrierRows( } // 케이스용 W80 - if (smokeBarrier.w80Qty > 0) { + if ((smokeBarrier.w80Qty ?? 0) > 0) { const w = calcWeight('EGI 0.8T', SMOKE_BARRIER_WIDTH, 3000); const code = getSLengthCode(3000, '연기차단재80'); rows.push({ @@ -537,7 +539,7 @@ export function calculateProductionSummary( } // 가이드레일 - 벽면형 - if (bendingInfo.guideRail.wall) { + if (bendingInfo.guideRail?.wall) { for (const ld of bendingInfo.guideRail.wall.lengthData) { if (ld.quantity <= 0) continue; addWeight(mapping.guideRailFinish, WALL_PART_WIDTH, ld.length, ld.quantity); @@ -552,7 +554,7 @@ export function calculateProductionSummary( } // 가이드레일 - 측면형 - if (bendingInfo.guideRail.side) { + if (bendingInfo.guideRail?.side) { for (const ld of bendingInfo.guideRail.side.lengthData) { if (ld.quantity <= 0) continue; addWeight(mapping.guideRailFinish, SIDE_PART_WIDTH, ld.length, ld.quantity); @@ -564,13 +566,13 @@ export function calculateProductionSummary( } // 하단마감재 - if (bendingInfo.bottomBar.length3000Qty > 0) { + if (bendingInfo.bottomBar?.length3000Qty > 0) { addWeight(mapping.bottomBarFinish, BOTTOM_BAR_WIDTH, 3000, bendingInfo.bottomBar.length3000Qty); if (mapping.bottomBarExtraFinish !== '없음' && mapping.bottomBarExtraFinish) { addWeight(mapping.bottomBarExtraFinish, EXTRA_FINISH_WIDTH, 3000, bendingInfo.bottomBar.length3000Qty); } } - if (bendingInfo.bottomBar.length4000Qty > 0) { + if (bendingInfo.bottomBar?.length4000Qty > 0) { addWeight(mapping.bottomBarFinish, BOTTOM_BAR_WIDTH, 4000, bendingInfo.bottomBar.length4000Qty); if (mapping.bottomBarExtraFinish !== '없음' && mapping.bottomBarExtraFinish) { addWeight(mapping.bottomBarExtraFinish, EXTRA_FINISH_WIDTH, 4000, bendingInfo.bottomBar.length4000Qty); @@ -578,7 +580,7 @@ export function calculateProductionSummary( } // 셔터박스 - for (const box of bendingInfo.shutterBox) { + for (const box of (bendingInfo.shutterBox ?? [])) { const boxRows = buildShutterBoxRows(box); for (const row of boxRows) { if (row.weight > 0) egiTotal += row.weight; // 셔터박스는 항상 EGI @@ -645,5 +647,10 @@ export function lookupLotNo( // 2. Fallback: prefix만으로 매칭 (첫 번째 일치 항목) const prefixKey = `BD-${prefix}-`; const fallbackKey = Object.keys(lotNoMap).find(k => k.startsWith(prefixKey)); - return fallbackKey ? lotNoMap[fallbackKey] : '-'; + if (fallbackKey) return lotNoMap[fallbackKey]; + + // 3. STOCK 원자재 LOT fallback (와일드카드 또는 _rawMaterialLot) + const wildcard = lotNoMap[`BD-${prefix}-*`]; + if (wildcard) return wildcard; + return lotNoMap['_rawMaterialLot'] || '-'; } diff --git a/src/components/production/WorkOrders/types.ts b/src/components/production/WorkOrders/types.ts index d47ff561..02841e31 100644 --- a/src/components/production/WorkOrders/types.ts +++ b/src/components/production/WorkOrders/types.ts @@ -485,7 +485,7 @@ export function transformApiToFrontend(api: WorkOrderApi): WorkOrder { return { id: String(api.id), workOrderNo: api.work_order_no, - lotNo: api.sales_order?.order_no || '-', + lotNo: api.sales_order?.options?.bending_lot?.lot_number || api.sales_order?.order_no || '-', processId: api.process_id, processName: api.process?.process_name || '-', processCode: api.process?.process_code || '-', diff --git a/src/components/production/WorkerScreen/InspectionInputModal.tsx b/src/components/production/WorkerScreen/InspectionInputModal.tsx index 1e9632a8..d2443877 100644 --- a/src/components/production/WorkerScreen/InspectionInputModal.tsx +++ b/src/components/production/WorkerScreen/InspectionInputModal.tsx @@ -547,8 +547,9 @@ export function InspectionInputModal({ workOrderId, }: InspectionInputModalProps) { // 템플릿 모드 여부 - // 절곡(bending)은 7제품 커스텀 폼 사용 → TemplateInspectionContent의 bending 셀 키와 연동 - const useTemplateMode = processType !== 'bending' && !!(templateData?.has_template && templateData.template); + // 절곡(bending/bending_wip)은 7제품 커스텀 폼 사용 → TemplateInspectionContent의 bending 셀 키와 연동 + const isBendingProcess = processType === 'bending' || processType === 'bending_wip'; + const useTemplateMode = !isBendingProcess && !!(templateData?.has_template && templateData.template); const [formData, setFormData] = useState({ productName, @@ -577,19 +578,21 @@ export function InspectionInputModal({ // API에서 절곡 제품 gap_points 동적 로딩 useEffect(() => { - if (!open || processType !== 'bending' || !workOrderId) return; + if (!open || !isBendingProcess || !workOrderId) return; let cancelled = false; getInspectionConfig(workOrderId).then(result => { if (cancelled) return; if (result.success && result.data?.items?.length) { + // 실제 품목 길이: workItemDimensions.height (예: 2438mm) 우선, 없으면 3000 폴백 + const actualLen = workItemDimensions?.height ? String(workItemDimensions.height) : '3000'; const displayMap: Record = { - guide_rail_wall: { label: '가이드레일 (벽면형)', len: '3000', wid: 'N/A' }, - guide_rail_side: { label: '가이드레일 (측면형)', len: '3000', wid: 'N/A' }, - case_box: { label: '케이스 (500X380)', len: '3000', wid: 'N/A' }, - bottom_bar: { label: '하단마감재 (60X40)', len: '3000', wid: 'N/A' }, - bottom_l_bar: { label: '하단L-BAR (17X60)', len: '3000', wid: 'N/A' }, - smoke_w50: { label: '연기차단재 (W50)', len: '3000', wid: '' }, - smoke_w80: { label: '연기차단재 (W80)', len: '3000', wid: '' }, + guide_rail_wall: { label: '가이드레일 (벽면형)', len: actualLen, wid: 'N/A' }, + guide_rail_side: { label: '가이드레일 (측면형)', len: actualLen, wid: 'N/A' }, + case_box: { label: '케이스 (500X380)', len: actualLen, wid: 'N/A' }, + bottom_bar: { label: '하단마감재 (60X40)', len: actualLen, wid: 'N/A' }, + bottom_l_bar: { label: '하단L-BAR (17X60)', len: actualLen, wid: 'N/A' }, + smoke_w50: { label: '연기차단재 (W50)', len: actualLen, wid: '' }, + smoke_w80: { label: '연기차단재 (W80)', len: actualLen, wid: '' }, }; const defs: BendingProductDef[] = result.data.items.map(item => { const d = displayMap[item.id] || { label: item.name, len: '-', wid: 'N/A' }; @@ -605,11 +608,11 @@ export function InspectionInputModal({ } }); return () => { cancelled = true; }; - }, [open, processType, workOrderId]); + }, [open, processType, workOrderId, workItemDimensions?.height]); // API 제품 정의 로딩 시 bendingProducts 갱신 (gap 개수 동기화) useEffect(() => { - if (!apiProductDefs || processType !== 'bending') return; + if (!apiProductDefs || !isBendingProcess) return; setBendingProducts(prev => { return apiProductDefs.map((def, idx) => { // 기존 입력값 보존 (ID 매칭 또는 인덱스 폴백) @@ -663,7 +666,7 @@ export function InspectionInputModal({ gapMeasured: def.gapPoints.map((_, gi) => saved.gapPoints?.[gi]?.measured || ''), }; })); - } else if (processType === 'bending' && initialData.judgment) { + } else if (isBendingProcess && initialData.judgment) { // 이전 형식 데이터 호환: products 배열 없이 저장된 경우 // judgment 값으로 제품별 상태 추론 (pass → 전체 양호) const restoredStatus: 'good' | 'bad' | null = @@ -751,7 +754,7 @@ export function InspectionInputModal({ return computeDynamicJudgment(templateData.template, dynamicFormValues, workItemDimensions); } // 절곡 7개 제품 전용 판정 - if (processType === 'bending') { + if (isBendingProcess) { let allGood = true; let allFilled = true; for (const p of bendingProducts) { @@ -785,7 +788,7 @@ export function InspectionInputModal({ }; // 절곡: products 배열을 성적서와 동일 포맷으로 저장 - if (processType === 'bending') { + if (isBendingProcess) { const products = bendingProducts.map((p, idx) => ({ id: p.id, bendingStatus: p.bendingStatus === 'good' ? '양호' : p.bendingStatus === 'bad' ? '불량' : null, @@ -841,22 +844,55 @@ export function InspectionInputModal({ )} onClick={() => { if (!formData.judgment) { - const w = workItemDimensions?.width || 1000; - const h = workItemDimensions?.height || 500; - setFormData((prev) => ({ - ...prev, - bendingStatus: 'good', - processingStatus: 'good', - sewingStatus: 'good', - assemblyStatus: 'good', - length: w, - width: h, - height1: h, - height2: h, - judgment: 'pass', - nonConformingContent: '', - })); + // 동적 템플릿 모드: 각 항목의 기준값을 사용하여 적합한 값 입력 + if (useTemplateMode && templateData?.template) { + const testValues: Record = {}; + for (const section of templateData.template.sections) { + for (const item of section.items) { + const fieldKey = `section_${section.id}_item_${item.id}`; + if (isNumericItem(item)) { + const design = resolveDesignValue(item, workItemDimensions); + testValues[fieldKey] = design ?? 100; + } else { + testValues[fieldKey] = 'ok'; + } + } + } + setDynamicFormValues(testValues); + } + if (!useTemplateMode) { + // 레거시 모드: 기존 로직 + const w = workItemDimensions?.width || 1000; + const h = workItemDimensions?.height || 500; + setFormData((prev) => ({ + ...prev, + bendingStatus: 'good', + processingStatus: 'good', + sewingStatus: 'good', + assemblyStatus: 'good', + length: w, + width: h, + height1: h, + height2: h, + judgment: 'pass', + nonConformingContent: '', + })); + // 절곡 7제품: 모든 제품 양호 + 도면치수와 동일한 측정값 입력 + if (isBendingProcess) { + setBendingProducts(effectiveProductDefs.map(def => ({ + id: def.id, + bendingStatus: 'good' as const, + lengthMeasured: def.lengthDesign || '', + widthMeasured: def.widthDesign || '', + gapMeasured: def.gapPoints.map(gp => gp.design || ''), + }))); + } + } } else { + // 초기화 + if (useTemplateMode) { + setDynamicFormValues({}); + } setFormData((prev) => ({ ...prev, bendingStatus: null, @@ -870,6 +906,10 @@ export function InspectionInputModal({ judgment: null, nonConformingContent: '', })); + // 절곡 7제품 초기화 + if (isBendingProcess) { + setBendingProducts(createInitialBendingProducts()); + } } }} > @@ -914,8 +954,8 @@ export function InspectionInputModal({ {/* ===== 레거시: 공정별 하드코딩 검사 항목 (템플릿 없을 때만 표시) ===== */} - {/* ===== 재고생산 (bending_wip) 검사 항목 ===== */} - {!useTemplateMode && processType === 'bending_wip' && ( + {/* ===== 재고생산 (bending_wip) 검사 항목 — 7제품 폼으로 통합됨 (위 절곡 검사 항목 참조) ===== */} + {false && processType === 'bending_wip' && ( <>
검모양 절곡상태 @@ -1139,7 +1179,7 @@ export function InspectionInputModal({ )} {/* ===== 절곡 검사 항목 (7개 제품별) ===== */} - {!useTemplateMode && processType === 'bending' && ( + {!useTemplateMode && isBendingProcess && (
{effectiveProductDefs.map((productDef, pIdx) => { const pState = bendingProducts[pIdx]; diff --git a/src/components/production/WorkerScreen/index.tsx b/src/components/production/WorkerScreen/index.tsx index 89d69681..8bac494c 100644 --- a/src/components/production/WorkerScreen/index.tsx +++ b/src/components/production/WorkerScreen/index.tsx @@ -22,6 +22,7 @@ function extractLengthFromName(name?: string | null): number { import { useState, useMemo, useCallback, useEffect } from 'react'; import dynamic from 'next/dynamic'; +import { useSearchParams, useRouter, usePathname } from 'next/navigation'; import { useSidebarCollapsed } from '@/stores/menuStore'; import { ClipboardList, PlayCircle, CheckCircle2, AlertTriangle, ChevronDown, ChevronUp, List } from 'lucide-react'; import { @@ -145,9 +146,20 @@ const PROCESS_STEPS: Record([]); const [isLoading, setIsLoading] = useState(true); - const [activeTab, setActiveTab] = useState(''); + const [activeTab, setActiveTabState] = useState(searchParams.get('tab') || ''); + + // 탭 변경 시 URL query parameter 동기화 (새로고침 시 탭 유지) + const setActiveTab = useCallback((tab: string) => { + setActiveTabState(tab); + const params = new URLSearchParams(searchParams.toString()); + params.set('tab', tab); + router.replace(`${pathname}?${params.toString()}`, { scroll: false }); + }, [searchParams, router, pathname]); const [bendingSubMode, setBendingSubMode] = useState<'normal' | 'wip'>('normal'); const [slatSubMode, setSlatSubMode] = useState<'normal' | 'jointbar'>('normal'); @@ -328,14 +340,21 @@ export default function WorkerScreen() { return groupedTabs.find((g) => g.group === groupName) || null; }, [activeTab, processListCache, groupedTabs]); - // 공정 목록 로드 후 첫 번째 그룹을 기본 선택 + // 공정 목록 로드 후 탭 선택 (URL 파라미터 우선, 없으면 첫 번째 그룹) useEffect(() => { - if (activeTab) return; - if (groupedTabs.length > 0) { - setActiveTab(groupedTabs[0].defaultProcessId); - } else if (!isLoading) { - setActiveTab('screen'); + if (groupedTabs.length === 0 && !isLoading) { + if (!activeTab) setActiveTabState('screen'); + return; } + if (groupedTabs.length === 0) return; + + // URL에 tab이 있고 유효한 탭이면 유지 + if (activeTab) { + const isValid = groupedTabs.some((g) => g.defaultProcessId === activeTab); + if (isValid) return; + } + // 없으면 첫 번째 그룹 선택 + setActiveTabState(groupedTabs[0].defaultProcessId); }, [groupedTabs, activeTab, isLoading]); // 선택된 공정의 ProcessTab 키 (mock 데이터 및 기존 로직 호환용) @@ -350,15 +369,19 @@ export default function WorkerScreen() { }, [activeTab, processListCache]); // 선택된 공정의 작업일지/검사성적서 설정 + // subProcessId가 선택되어 있으면 자식 공정의 설정 사용 const activeProcessSettings = useMemo(() => { - const process = processListCache.find((p) => p.id === activeTab); + const effectiveId = subProcessId !== 'all' ? subProcessId : activeTab; + const process = processListCache.find((p) => p.id === effectiveId); + // 자식 공정에 설정이 없으면 부모 공정 폴백 + const parentProcess = processListCache.find((p) => p.id === activeTab); return { - needsWorkLog: process?.needsWorkLog ?? false, - hasDocumentTemplate: !!process?.documentTemplateId, - workLogTemplateId: process?.workLogTemplateId, - workLogTemplateName: process?.workLogTemplateName, + needsWorkLog: process?.needsWorkLog ?? parentProcess?.needsWorkLog ?? false, + hasDocumentTemplate: !!(process?.documentTemplateId ?? parentProcess?.documentTemplateId), + workLogTemplateId: process?.workLogTemplateId ?? parentProcess?.workLogTemplateId, + workLogTemplateName: process?.workLogTemplateName ?? parentProcess?.workLogTemplateName, }; - }, [activeTab, processListCache]); + }, [activeTab, subProcessId, processListCache]); // activeTab 변경 시 해당 공정의 중간검사 설정 조회 useEffect(() => { @@ -1329,8 +1352,13 @@ export default function WorkerScreen() { // ===== 재공품 감지 ===== const hasWipItems = useMemo(() => { - return activeProcessTabKey === 'bending' && workItems.some(item => item.isWip); - }, [activeProcessTabKey, workItems]); + if (activeProcessTabKey !== 'bending') return false; + // 1. workItems에서 isWip 체크 + if (workItems.some(item => item.isWip)) return true; + // 2. Fallback: 선택된 작업지시의 프로젝트명/수주번호로 WIP 판별 + const selectedWo = filteredWorkOrders.find(wo => wo.id === selectedSidebarOrderId); + return !!(selectedWo && (selectedWo.projectName === '재고생산' || selectedWo.salesOrderNo?.startsWith('STK'))); + }, [activeProcessTabKey, workItems, filteredWorkOrders, selectedSidebarOrderId]); // ===== 조인트바 감지 ===== const hasJointBarItems = useMemo(() => { @@ -1620,33 +1648,22 @@ export default function WorkerScreen() { {(hasWipItems || activeProcessSettings.needsWorkLog || activeProcessSettings.hasDocumentTemplate) && (
- {hasWipItems ? ( + {(hasWipItems || activeProcessSettings.needsWorkLog) && ( + + )} + {(hasWipItems || activeProcessSettings.hasDocumentTemplate) && ( - ) : ( - <> - {activeProcessSettings.needsWorkLog && ( - - )} - {activeProcessSettings.hasDocumentTemplate && ( - - )} - )}