feat: [production] 절곡 중간검사 입력 모달 — 7개 제품 항목 통합 및 성적서 데이터 연동
- InspectionInputModal: 절곡 전용 7개 제품별 입력 폼 (절곡상태/길이/너비/간격) - TemplateInspectionContent: products 배열 → bending cellValues 자동 매핑 - 제품 ID 3단계 매칭 (정규화→키워드→인덱스 폴백) - 절곡 작업지시서 bending 섹션 개선
This commit is contained in:
@@ -73,6 +73,86 @@ interface InspectionInputModalProps {
|
||||
workItemDimensions?: { width?: number; height?: number };
|
||||
}
|
||||
|
||||
// ===== 절곡 7개 제품 검사 항목 (BendingInspectionContent의 INITIAL_PRODUCTS와 동일 구조) =====
|
||||
interface BendingGapPointDef {
|
||||
point: string;
|
||||
design: string;
|
||||
}
|
||||
|
||||
interface BendingProductDef {
|
||||
id: string;
|
||||
label: string;
|
||||
lengthDesign: string;
|
||||
widthDesign: string;
|
||||
gapPoints: BendingGapPointDef[];
|
||||
}
|
||||
|
||||
const BENDING_PRODUCTS: BendingProductDef[] = [
|
||||
{
|
||||
id: 'guide-rail-wall', label: '가이드레일 (벽면형)', lengthDesign: '3000', widthDesign: 'N/A',
|
||||
gapPoints: [
|
||||
{ point: '①', design: '30' }, { point: '②', design: '80' },
|
||||
{ point: '③', design: '45' }, { point: '④', design: '40' }, { point: '⑤', design: '34' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'guide-rail-side', label: '가이드레일 (측면형)', lengthDesign: '3000', widthDesign: 'N/A',
|
||||
gapPoints: [
|
||||
{ point: '①', design: '28' }, { point: '②', design: '75' },
|
||||
{ point: '③', design: '42' }, { point: '④', design: '38' }, { point: '⑤', design: '32' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'case', label: '케이스 (500X380)', lengthDesign: '3000', widthDesign: 'N/A',
|
||||
gapPoints: [
|
||||
{ point: '①', design: '380' }, { point: '②', design: '50' },
|
||||
{ point: '③', design: '240' }, { point: '④', design: '50' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'bottom-finish', label: '하단마감재 (60X40)', lengthDesign: '3000', widthDesign: 'N/A',
|
||||
gapPoints: [
|
||||
{ point: '②', design: '60' }, { point: '②', design: '64' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'bottom-l-bar', label: '하단L-BAR (17X60)', lengthDesign: '3000', widthDesign: 'N/A',
|
||||
gapPoints: [
|
||||
{ point: '①', design: '17' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'smoke-w50', label: '연기차단재 (W50)', lengthDesign: '3000', widthDesign: '',
|
||||
gapPoints: [
|
||||
{ point: '①', design: '50' }, { point: '②', design: '12' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'smoke-w80', label: '연기차단재 (W80)', lengthDesign: '3000', widthDesign: '',
|
||||
gapPoints: [
|
||||
{ point: '①', design: '80' }, { point: '②', design: '12' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
interface BendingProductState {
|
||||
id: string;
|
||||
bendingStatus: 'good' | 'bad' | null;
|
||||
lengthMeasured: string;
|
||||
widthMeasured: string;
|
||||
gapMeasured: string[];
|
||||
}
|
||||
|
||||
function createInitialBendingProducts(): BendingProductState[] {
|
||||
return BENDING_PRODUCTS.map(p => ({
|
||||
id: p.id,
|
||||
bendingStatus: null,
|
||||
lengthMeasured: '',
|
||||
widthMeasured: '',
|
||||
gapMeasured: p.gapPoints.map(() => ''),
|
||||
}));
|
||||
}
|
||||
|
||||
const PROCESS_TITLES: Record<InspectionProcessType, string> = {
|
||||
screen: '# 스크린 중간검사',
|
||||
slat: '# 슬랫 중간검사',
|
||||
@@ -463,7 +543,8 @@ export function InspectionInputModal({
|
||||
workItemDimensions,
|
||||
}: InspectionInputModalProps) {
|
||||
// 템플릿 모드 여부
|
||||
const useTemplateMode = !!(templateData?.has_template && templateData.template);
|
||||
// 절곡(bending)은 7제품 커스텀 폼 사용 → TemplateInspectionContent의 bending 셀 키와 연동
|
||||
const useTemplateMode = processType !== 'bending' && !!(templateData?.has_template && templateData.template);
|
||||
|
||||
const [formData, setFormData] = useState<InspectionData>({
|
||||
productName,
|
||||
@@ -475,11 +556,14 @@ export function InspectionInputModal({
|
||||
// 동적 폼 값 (템플릿 모드용)
|
||||
const [dynamicFormValues, setDynamicFormValues] = useState<Record<string, unknown>>({});
|
||||
|
||||
// 절곡용 간격 포인트 초기화
|
||||
// 절곡용 간격 포인트 초기화 (레거시 — bending_wip 등에서 사용)
|
||||
const [gapPoints, setGapPoints] = useState<{ left: number | null; right: number | null }[]>(
|
||||
Array(5).fill(null).map(() => ({ left: null, right: null }))
|
||||
);
|
||||
|
||||
// 절곡 7개 제품별 상태 (bending 전용)
|
||||
const [bendingProducts, setBendingProducts] = useState<BendingProductState[]>(createInitialBendingProducts);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
// initialData가 있으면 기존 저장 데이터로 복원
|
||||
@@ -495,6 +579,29 @@ export function InspectionInputModal({
|
||||
} else {
|
||||
setGapPoints(Array(5).fill(null).map(() => ({ left: null, right: null })));
|
||||
}
|
||||
// 절곡 제품별 데이터 복원
|
||||
const savedProducts = (initialData as unknown as Record<string, unknown>).products as Array<{
|
||||
id: string;
|
||||
bendingStatus: string;
|
||||
lengthMeasured: string;
|
||||
widthMeasured: string;
|
||||
gapPoints: Array<{ point: string; designValue: string; measured: string }>;
|
||||
}> | undefined;
|
||||
if (savedProducts && Array.isArray(savedProducts)) {
|
||||
setBendingProducts(BENDING_PRODUCTS.map((def, idx) => {
|
||||
const saved = savedProducts.find(sp => sp.id === def.id);
|
||||
if (!saved) return { id: def.id, bendingStatus: null, lengthMeasured: '', widthMeasured: '', gapMeasured: def.gapPoints.map(() => '') };
|
||||
return {
|
||||
id: def.id,
|
||||
bendingStatus: saved.bendingStatus === '양호' ? 'good' : saved.bendingStatus === '불량' ? 'bad' : (saved.bendingStatus as 'good' | 'bad' | null),
|
||||
lengthMeasured: saved.lengthMeasured || '',
|
||||
widthMeasured: saved.widthMeasured || '',
|
||||
gapMeasured: def.gapPoints.map((_, gi) => saved.gapPoints?.[gi]?.measured || ''),
|
||||
};
|
||||
}));
|
||||
} else {
|
||||
setBendingProducts(createInitialBendingProducts());
|
||||
}
|
||||
// 동적 폼 값 복원 (템플릿 기반 검사 데이터)
|
||||
if (initialData.templateValues) {
|
||||
setDynamicFormValues(initialData.templateValues);
|
||||
@@ -554,17 +661,30 @@ export function InspectionInputModal({
|
||||
}
|
||||
|
||||
setGapPoints(Array(5).fill(null).map(() => ({ left: null, right: null })));
|
||||
setBendingProducts(createInitialBendingProducts());
|
||||
setDynamicFormValues({});
|
||||
}
|
||||
}, [open, productName, specification, processType, initialData]);
|
||||
|
||||
// 자동 판정 계산 (템플릿 모드 vs 레거시 모드)
|
||||
// 자동 판정 계산 (템플릿 모드 vs 절곡 7제품 모드 vs 레거시 모드)
|
||||
const autoJudgment = useMemo(() => {
|
||||
if (useTemplateMode && templateData?.template) {
|
||||
return computeDynamicJudgment(templateData.template, dynamicFormValues, workItemDimensions);
|
||||
}
|
||||
// 절곡 7개 제품 전용 판정
|
||||
if (processType === 'bending') {
|
||||
let allGood = true;
|
||||
let allFilled = true;
|
||||
for (const p of bendingProducts) {
|
||||
if (p.bendingStatus === 'bad') return 'fail';
|
||||
if (p.bendingStatus !== 'good') { allGood = false; allFilled = false; }
|
||||
if (!p.lengthMeasured) allFilled = false;
|
||||
}
|
||||
if (allGood && allFilled) return 'pass';
|
||||
return null;
|
||||
}
|
||||
return computeJudgment(processType, formData);
|
||||
}, [useTemplateMode, templateData, dynamicFormValues, workItemDimensions, processType, formData]);
|
||||
}, [useTemplateMode, templateData, dynamicFormValues, workItemDimensions, processType, formData, bendingProducts]);
|
||||
|
||||
// 판정값 자동 동기화
|
||||
useEffect(() => {
|
||||
@@ -575,13 +695,32 @@ export function InspectionInputModal({
|
||||
}, [autoJudgment]);
|
||||
|
||||
const handleComplete = () => {
|
||||
const data: InspectionData = {
|
||||
const baseData: InspectionData = {
|
||||
...formData,
|
||||
gapPoints: processType === 'bending' ? gapPoints : undefined,
|
||||
// 동적 폼 값을 templateValues로 병합
|
||||
...(useTemplateMode ? { templateValues: dynamicFormValues } : {}),
|
||||
};
|
||||
onComplete(data);
|
||||
|
||||
// 절곡: products 배열을 성적서와 동일 포맷으로 저장
|
||||
if (processType === 'bending') {
|
||||
const products = bendingProducts.map((p, idx) => ({
|
||||
id: p.id,
|
||||
bendingStatus: p.bendingStatus === 'good' ? '양호' : p.bendingStatus === 'bad' ? '불량' : null,
|
||||
lengthMeasured: p.lengthMeasured,
|
||||
widthMeasured: p.widthMeasured,
|
||||
gapPoints: BENDING_PRODUCTS[idx].gapPoints.map((gp, gi) => ({
|
||||
point: gp.point,
|
||||
designValue: gp.design,
|
||||
measured: p.gapMeasured[gi] || '',
|
||||
})),
|
||||
}));
|
||||
const data = { ...baseData, products } as unknown as InspectionData;
|
||||
onComplete(data);
|
||||
onOpenChange(false);
|
||||
return;
|
||||
}
|
||||
|
||||
onComplete(baseData);
|
||||
onOpenChange(false);
|
||||
};
|
||||
|
||||
@@ -866,75 +1005,96 @@ export function InspectionInputModal({
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* ===== 절곡 검사 항목 ===== */}
|
||||
{/* ===== 절곡 검사 항목 (7개 제품별) ===== */}
|
||||
{!useTemplateMode && processType === 'bending' && (
|
||||
<>
|
||||
<div className="space-y-1.5">
|
||||
<span className="text-sm font-bold">검모양 절곡상태</span>
|
||||
<StatusToggle
|
||||
value={formData.bendingStatus || null}
|
||||
onChange={(v) => setFormData((prev) => ({ ...prev, bendingStatus: v }))}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="space-y-1.5">
|
||||
<span className="text-sm font-bold">길이 ({formatDimension(workItemDimensions?.width)})</span>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder={formatDimension(workItemDimensions?.width)}
|
||||
value={formData.length ?? ''}
|
||||
onChange={(e) => handleNumberChange('length', e.target.value)}
|
||||
className="h-11 rounded-lg border-gray-300"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<span className="text-sm font-bold">너비 (N/A)</span>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="N/A"
|
||||
value={formData.width ?? 'N/A'}
|
||||
readOnly
|
||||
className="h-11 bg-gray-100 border-gray-300 rounded-lg"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<span className="text-sm font-bold">간격</span>
|
||||
{gapPoints.map((point, index) => (
|
||||
<div key={index} className="grid grid-cols-3 gap-2 items-center">
|
||||
<span className="text-gray-500 text-sm font-medium">⑤{index + 1}</span>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder={String(30 + index * 10)}
|
||||
value={point.left ?? ''}
|
||||
onChange={(e) => {
|
||||
const newPoints = [...gapPoints];
|
||||
newPoints[index] = {
|
||||
...newPoints[index],
|
||||
left: e.target.value === '' ? null : parseFloat(e.target.value),
|
||||
};
|
||||
setGapPoints(newPoints);
|
||||
}}
|
||||
className="h-11 rounded-lg border-gray-300"
|
||||
/>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder={String(30 + index * 10)}
|
||||
value={point.right ?? ''}
|
||||
onChange={(e) => {
|
||||
const newPoints = [...gapPoints];
|
||||
newPoints[index] = {
|
||||
...newPoints[index],
|
||||
right: e.target.value === '' ? null : parseFloat(e.target.value),
|
||||
};
|
||||
setGapPoints(newPoints);
|
||||
}}
|
||||
className="h-11 rounded-lg border-gray-300"
|
||||
/>
|
||||
<div className="space-y-4">
|
||||
{BENDING_PRODUCTS.map((productDef, pIdx) => {
|
||||
const pState = bendingProducts[pIdx];
|
||||
if (!pState) return null;
|
||||
|
||||
const updateProduct = (updates: Partial<BendingProductState>) => {
|
||||
setBendingProducts(prev => prev.map((p, i) => i === pIdx ? { ...p, ...updates } : p));
|
||||
};
|
||||
|
||||
return (
|
||||
<div key={productDef.id} className={cn(pIdx > 0 && 'border-t border-gray-200 pt-4')}>
|
||||
{/* 제품명 헤더 */}
|
||||
<div className="mb-3">
|
||||
<span className="text-sm font-bold text-gray-900">
|
||||
{pIdx + 1}. {productDef.label}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 절곡상태 */}
|
||||
<div className="space-y-1.5 mb-3">
|
||||
<span className="text-xs text-gray-500 font-medium">절곡상태</span>
|
||||
<StatusToggle
|
||||
value={pState.bendingStatus}
|
||||
onChange={(v) => updateProduct({ bendingStatus: v })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 길이 / 너비 */}
|
||||
<div className="grid grid-cols-2 gap-3 mb-3">
|
||||
<div className="space-y-1.5">
|
||||
<span className="text-xs text-gray-500 font-medium">길이 ({productDef.lengthDesign})</span>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder={productDef.lengthDesign}
|
||||
value={pState.lengthMeasured}
|
||||
onChange={(e) => updateProduct({ lengthMeasured: e.target.value })}
|
||||
className="h-10 rounded-lg border-gray-300 text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<span className="text-xs text-gray-500 font-medium">너비 ({productDef.widthDesign || '-'})</span>
|
||||
{productDef.widthDesign === 'N/A' ? (
|
||||
<Input
|
||||
type="text"
|
||||
value="N/A"
|
||||
readOnly
|
||||
className="h-10 bg-gray-100 border-gray-300 rounded-lg text-sm"
|
||||
/>
|
||||
) : (
|
||||
<Input
|
||||
type="number"
|
||||
placeholder={productDef.widthDesign || '-'}
|
||||
value={pState.widthMeasured}
|
||||
onChange={(e) => updateProduct({ widthMeasured: e.target.value })}
|
||||
className="h-10 rounded-lg border-gray-300 text-sm"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 간격 포인트 */}
|
||||
{productDef.gapPoints.length > 0 && (
|
||||
<div className="space-y-1.5">
|
||||
<span className="text-xs text-gray-500 font-medium">간격</span>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{productDef.gapPoints.map((gp, gi) => (
|
||||
<div key={gi} className="flex items-center gap-1.5">
|
||||
<span className="text-xs text-gray-400 w-14 shrink-0">{gp.point} ({gp.design})</span>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder={gp.design}
|
||||
value={pState.gapMeasured[gi] || ''}
|
||||
onChange={(e) => {
|
||||
const newGaps = [...pState.gapMeasured];
|
||||
newGaps[gi] = e.target.value;
|
||||
updateProduct({ gapMeasured: newGaps });
|
||||
}}
|
||||
className="h-9 rounded-lg border-gray-300 text-sm"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 부적합 내용 */}
|
||||
|
||||
Reference in New Issue
Block a user