fix: [inspection] 절곡 검사성적서 재공품 대응 통합 수정
- 검사부위 공백 수정 (템플릿 컬럼 "부위" 라벨 매칭) - hasWipItems 판정 보완 (sidebar order fallback) - bending_wip 7제품 폼 통합 (products 배열 저장) - 도면치수 실제 품목 길이 반영 (3000 하드코딩 제거) - 테스트입력 버튼 7제품 데이터 채우기 - 하단 버튼 분리 유지 (작업일지/검사성적서) - STOCK 단일부품 해당 부품만 검사항목 표시 - bendingInfo 기반 동적 검사 제품 생성 - 작업일지 LOT NO 원자재 투입 로트번호 표시
This commit is contained in:
@@ -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