fix: [inspection] 절곡 검사성적서 재공품 대응 통합 수정
- 검사부위 공백 수정 (템플릿 컬럼 "부위" 라벨 매칭) - hasWipItems 판정 보완 (sidebar order fallback) - bending_wip 7제품 폼 통합 (products 배열 저장) - 도면치수 실제 품목 길이 반영 (3000 하드코딩 제거) - 테스트입력 버튼 7제품 데이터 채우기 - 하단 버튼 분리 유지 (작업일지/검사성적서) - STOCK 단일부품 해당 부품만 검사항목 표시 - bendingInfo 기반 동적 검사 제품 생성 - 작업일지 LOT NO 원자재 투입 로트번호 표시
This commit is contained in:
@@ -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<ProductRow, 'bendingStatus' | 'lengthMeasured' | 'widthMeasured'>[] = [
|
||||
{
|
||||
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<ProductRow, 'bendingStatus' | 'lengthMeasured' | 'widthMeasured'>;
|
||||
|
||||
/** 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<InspectionContentRef, BendingInspectionContentProps>(function BendingInspectionContent({
|
||||
data: order,
|
||||
@@ -147,34 +217,32 @@ export const BendingInspectionContent = forwardRef<InspectionContentRef, Bending
|
||||
const today = getToday();
|
||||
const { documentNo, primaryAssignee } = getOrderInfo(order);
|
||||
|
||||
const [products, setProducts] = useState<ProductRow[]>(() =>
|
||||
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<ProductRow[]>([]);
|
||||
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<string, unknown>).products as Array<{
|
||||
const savedProducts = (itemData as unknown as Record<string, unknown> | undefined)?.products as Array<{
|
||||
id: string;
|
||||
bendingStatus: CheckStatus;
|
||||
lengthMeasured: string;
|
||||
@@ -182,53 +250,35 @@ export const BendingInspectionContent = forwardRef<InspectionContentRef, Bending
|
||||
gapPoints: Array<{ point: string; designValue: string; measured: string }>;
|
||||
}> | 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<string, unknown>).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<string, unknown> | 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<InspectionContentRef, Bending
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="border border-gray-400 bg-gray-50 px-3 py-2 font-medium w-24">제품명</td>
|
||||
<td className="border border-gray-400 px-3 py-2">슬랫</td>
|
||||
<td className="border border-gray-400 px-3 py-2">{productCode}</td>
|
||||
<td className="border border-gray-400 bg-gray-50 px-3 py-2 font-medium w-28">제품 LOT NO</td>
|
||||
<td className="border border-gray-400 px-3 py-2">{order.lotNo || '-'}</td>
|
||||
</tr>
|
||||
@@ -315,9 +365,9 @@ export const BendingInspectionContent = forwardRef<InspectionContentRef, Bending
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border border-gray-400 bg-gray-50 px-3 py-2 font-medium">제품명</td>
|
||||
<td className="border border-gray-400 px-3 py-2">KWE01</td>
|
||||
<td className="border border-gray-400 px-3 py-2">{productCode}</td>
|
||||
<td className="border border-gray-400 bg-gray-50 px-3 py-2 font-medium">마감유형</td>
|
||||
<td className="border border-gray-400 px-3 py-2">소니자감</td>
|
||||
<td className="border border-gray-400 px-3 py-2">{finishMaterial || '-'}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -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<InspectionContentRef, Bend
|
||||
const today = getToday();
|
||||
const { documentNo, primaryAssignee } = getOrderInfo(order);
|
||||
|
||||
const rowCount = workItems?.length || order.items?.length || DEFAULT_ROW_COUNT;
|
||||
const bendingInfo = order.bendingInfo as BendingInfoExtended | undefined;
|
||||
|
||||
const [rows, setRows] = useState<InspectionRow[]>(() =>
|
||||
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<InspectionRow[]>([]);
|
||||
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<InspectionContentRef, Bend
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border border-gray-400 bg-gray-50 px-3 py-2 font-medium">길이</td>
|
||||
<td className="border border-gray-400 px-3 py-2">3,000 mm</td>
|
||||
<td className="border border-gray-400 px-3 py-2">{(() => {
|
||||
const len = extractLengthFromItemName(order.items?.[0]?.productName);
|
||||
return len !== '-' ? `${Number(len).toLocaleString()} mm` : '-';
|
||||
})()}</td>
|
||||
<td className="border border-gray-400 bg-gray-50 px-3 py-2 font-medium">수량</td>
|
||||
<td className="border border-gray-400 px-3 py-2">{order.items?.reduce((sum, item) => sum + item.quantity, 0) || 0} EA</td>
|
||||
</tr>
|
||||
@@ -243,7 +270,7 @@ export const BendingWipInspectionContent = forwardRef<InspectionContentRef, Bend
|
||||
<thead>
|
||||
<tr className="bg-gray-100">
|
||||
<th className="border border-gray-400 p-1 w-8" rowSpan={2}>No.</th>
|
||||
<th className="border border-gray-400 p-1 w-20" rowSpan={2}>제품명</th>
|
||||
<th className="border border-gray-400 p-1" rowSpan={2}>검사 부위</th>
|
||||
<th className="border border-gray-400 p-1 w-16" rowSpan={2}>절곡상태<br/>겉모양</th>
|
||||
<th className="border border-gray-400 p-1" colSpan={2}>길이 (mm)</th>
|
||||
<th className="border border-gray-400 p-1" colSpan={2}>너비 (mm)</th>
|
||||
@@ -266,9 +293,7 @@ export const BendingWipInspectionContent = forwardRef<InspectionContentRef, Bend
|
||||
return (
|
||||
<tr key={row.id}>
|
||||
<td className="border border-gray-400 p-1 text-center">{row.id}</td>
|
||||
<td className="border border-gray-400 p-1">
|
||||
<input type="text" value={row.productName} onChange={(e) => handleInputChange(row.id, 'productName', e.target.value)} disabled={readOnly} className={INPUT_CLASS} placeholder="-" />
|
||||
</td>
|
||||
<td className="border border-gray-400 p-1">{row.partName || '-'}</td>
|
||||
<CheckStatusCell value={row.processStatus} onToggle={(v) => handleStatusChange(row.id, v)} readOnly={readOnly} />
|
||||
<td className="border border-gray-400 p-1 text-center">{row.lengthDesign}</td>
|
||||
<td className="border border-gray-400 p-1">
|
||||
|
||||
@@ -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<string, string>; // BD-{prefix}-{lengthCode} → LOT NO
|
||||
bendingImages?: Record<string, string>; // 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<string, string[]> = {
|
||||
'본체': ['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 (
|
||||
<div className="p-6 bg-white">
|
||||
{/* ===== 헤더 영역 ===== */}
|
||||
@@ -166,23 +197,23 @@ export function BendingWorkLogContent({ data: order, lotNoMap, bendingImages }:
|
||||
bendingInfo={bendingInfo!}
|
||||
mapping={mapping}
|
||||
lotNo={order.lotNo}
|
||||
lotNoMap={lotNoMap}
|
||||
lotNoMap={effectiveLotNoMap}
|
||||
bendingImages={bendingImages}
|
||||
/>
|
||||
<BottomBarSection
|
||||
bendingInfo={bendingInfo!}
|
||||
mapping={mapping}
|
||||
lotNoMap={lotNoMap}
|
||||
lotNoMap={effectiveLotNoMap}
|
||||
bendingImages={bendingImages}
|
||||
/>
|
||||
<ShutterBoxSection
|
||||
bendingInfo={bendingInfo!}
|
||||
lotNoMap={lotNoMap}
|
||||
lotNoMap={effectiveLotNoMap}
|
||||
bendingImages={bendingImages}
|
||||
/>
|
||||
<SmokeBarrierSection
|
||||
bendingInfo={bendingInfo!}
|
||||
lotNoMap={lotNoMap}
|
||||
lotNoMap={effectiveLotNoMap}
|
||||
bendingImages={bendingImages}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -400,14 +400,20 @@ export const TemplateInspectionContent = forwardRef<InspectionContentRef, Templa
|
||||
const bi = order.bendingInfo as BendingInfoExtended | undefined;
|
||||
const wallLen = bi?.guideRail?.wall?.lengthData?.[0]?.length;
|
||||
const sideLen = bi?.guideRail?.side?.lengthData?.[0]?.length;
|
||||
// 품목명에서 길이 추출 (예: "가이드레일(측면) 본체(철재) 2438mm" → 2438)
|
||||
const itemLen = order.items?.[0]?.productName?.match(/(\d{3,5})\s*mm/i)?.[1];
|
||||
// 실제 길이: bendingInfo > 품목명 > 기본값
|
||||
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<string, { name: string; type: string; len: string; wid: string }> = {
|
||||
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<InspectionContentRef, Templa
|
||||
const cell = cellValues[cellKey];
|
||||
const label = col.label.trim();
|
||||
|
||||
// 1. 분류/제품명 (text)
|
||||
if (col.column_type === 'text' && (label.includes('분류') || label.includes('제품명'))) {
|
||||
// 1. 분류/제품명/검사부위 (text)
|
||||
if (col.column_type === 'text' && (label.includes('분류') || label.includes('제품명') || label.includes('부위'))) {
|
||||
return (
|
||||
<td key={col.id} rowSpan={rs}
|
||||
className="border border-gray-400 px-2 py-1.5 text-center align-middle text-xs"
|
||||
|
||||
@@ -78,17 +78,37 @@ function PartTable({ title, rows, imageUrl, lotNo: _lotNo, baseSize, lotNoMap }:
|
||||
}
|
||||
|
||||
export function GuideRailSection({ bendingInfo, mapping, lotNo, lotNoMap, bendingImages }: GuideRailSectionProps) {
|
||||
const { wall, side } = bendingInfo.guideRail;
|
||||
const guideRail = bendingInfo.guideRail;
|
||||
if (!guideRail) return null;
|
||||
const { wall, side } = guideRail;
|
||||
const productCode = bendingInfo.productCode;
|
||||
const stockFilter = bendingInfo.stockPartFilter;
|
||||
|
||||
const wallRows = wall
|
||||
let wallRows = wall
|
||||
? buildWallGuideRailRows(wall.lengthData, wall.baseDimension || '135*80', mapping, productCode)
|
||||
: [];
|
||||
|
||||
const sideRows = side
|
||||
let sideRows = side
|
||||
? buildSideGuideRailRows(side.lengthData, side.baseDimension || '135*130', mapping, productCode)
|
||||
: [];
|
||||
|
||||
// STOCK 단일부품 생산: stockPartFilter에 해당하는 부품만 표시
|
||||
if (stockFilter && stockFilter.length > 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 (
|
||||
|
||||
@@ -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형", "마감재" 등
|
||||
}>;
|
||||
}
|
||||
|
||||
// 재질 매핑 결과
|
||||
|
||||
@@ -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'] || '-';
|
||||
}
|
||||
|
||||
@@ -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 || '-',
|
||||
|
||||
@@ -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<InspectionData>({
|
||||
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<string, { label: string; len: string; wid: string }> = {
|
||||
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<string, unknown> = {};
|
||||
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' && (
|
||||
<>
|
||||
<div className="space-y-1.5">
|
||||
<span className="text-sm font-bold">검모양 절곡상태</span>
|
||||
@@ -1139,7 +1179,7 @@ export function InspectionInputModal({
|
||||
)}
|
||||
|
||||
{/* ===== 절곡 검사 항목 (7개 제품별) ===== */}
|
||||
{!useTemplateMode && processType === 'bending' && (
|
||||
{!useTemplateMode && isBendingProcess && (
|
||||
<div className="space-y-4">
|
||||
{effectiveProductDefs.map((productDef, pIdx) => {
|
||||
const pState = bendingProducts[pIdx];
|
||||
|
||||
@@ -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<string, { name: string; isMaterialInput: boolean; is
|
||||
export default function WorkerScreen() {
|
||||
// ===== 상태 관리 =====
|
||||
const sidebarCollapsed = useSidebarCollapsed();
|
||||
const searchParams = useSearchParams();
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const [workOrders, setWorkOrders] = useState<WorkOrder[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [activeTab, setActiveTab] = useState<string>('');
|
||||
const [activeTab, setActiveTabState] = useState<string>(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) && (
|
||||
<div className={`fixed bottom-4 left-4 right-4 px-4 py-3 bg-background/95 backdrop-blur rounded-xl border shadow-lg z-50 transition-all duration-300 md:bottom-6 md:px-6 md:right-[24px] ${sidebarCollapsed ? 'md:left-[120px]' : 'md:left-[280px]'}`}>
|
||||
<div className="flex gap-2 md:gap-3">
|
||||
{hasWipItems ? (
|
||||
{(hasWipItems || activeProcessSettings.needsWorkLog) && (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleWorkLog}
|
||||
className="flex-1 py-5 md:py-6 text-sm md:text-base font-medium"
|
||||
>
|
||||
작업일지 보기
|
||||
</Button>
|
||||
)}
|
||||
{(hasWipItems || activeProcessSettings.hasDocumentTemplate) && (
|
||||
<Button
|
||||
onClick={handleInspection}
|
||||
className="flex-1 py-5 md:py-6 text-sm md:text-base font-medium bg-gray-900 hover:bg-gray-800"
|
||||
>
|
||||
작업일지 및 검사성적서
|
||||
검사성적서 보기
|
||||
</Button>
|
||||
) : (
|
||||
<>
|
||||
{activeProcessSettings.needsWorkLog && (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleWorkLog}
|
||||
className="flex-1 py-5 md:py-6 text-sm md:text-base font-medium"
|
||||
>
|
||||
작업일지 보기
|
||||
</Button>
|
||||
)}
|
||||
{activeProcessSettings.hasDocumentTemplate && (
|
||||
<Button
|
||||
onClick={handleInspection}
|
||||
className="flex-1 py-5 md:py-6 text-sm md:text-base font-medium bg-gray-900 hover:bg-gray-800"
|
||||
>
|
||||
검사성적서 보기
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user