fix(WEB): 개소 그룹핑 키를 order_node_id 기반으로 변경 + 검사 step 동적 처리

- WorkerScreen/actions.ts: 그룹핑 키를 floor_code/symbol_code → order_node_id 우선으로 변경
- WorkOrderDetail.tsx: 개소 그룹핑을 orderNodeId 기반으로 단순화
- WorkerScreen/index.tsx: 검사 step name 하드코딩('중간검사') 제거, 동적 step name 사용
- InspectionReportModal.tsx: inspectionDataMap 빈 Map 허용 수정
This commit is contained in:
2026-02-21 07:19:38 +09:00
parent 77cad7a83a
commit c369f9142f
4 changed files with 52 additions and 58 deletions

View File

@@ -514,35 +514,15 @@ export function WorkOrderDetail({ orderId }: WorkOrderDetailProps) {
// 개소(층/부호)별로 그룹화
const nodeGroups = new Map<string, { label: string; items: typeof order.items }>();
// 모든 아이템이 동일 그룹으로 들어가는지 확인
const uniqueKeys = new Set(order.items.map(item =>
item.floorCode !== '-' ? item.floorCode : String(item.orderNodeId ?? 'none')
));
const allSameGroup = uniqueKeys.size <= 1;
const locationCount = order.shutterCount || 1;
if (allSameGroup && locationCount > 1) {
// 인덱스 기반 균등 분배
const itemsPerLoc = Math.ceil(order.items.length / locationCount);
for (let loc = 0; loc < locationCount; loc++) {
const start = loc * itemsPerLoc;
const end = Math.min(start + itemsPerLoc, order.items.length);
if (start >= order.items.length) break;
const key = `loc-${loc}`;
nodeGroups.set(key, {
label: `개소 ${loc + 1}`,
items: order.items.slice(start, end),
});
}
} else {
for (const item of order.items) {
const key = item.floorCode !== '-' ? item.floorCode : (item.orderNodeId != null ? String(item.orderNodeId) : 'none');
const label = item.floorCode !== '-' ? item.floorCode : item.orderNodeName;
if (!nodeGroups.has(key)) {
nodeGroups.set(key, { label, items: [] });
}
nodeGroups.get(key)!.items.push(item);
// order_node_id 기반 그룹핑 (floor_code/symbol_code는 표시용)
for (const item of order.items) {
const floorLabel = item.floorCode !== '-' ? item.floorCode : '';
const key = item.orderNodeId != null ? String(item.orderNodeId) : (floorLabel || 'none');
const label = floorLabel || item.orderNodeName || `개소 ${nodeGroups.size + 1}`;
if (!nodeGroups.has(key)) {
nodeGroups.set(key, { label, items: [] });
}
nodeGroups.get(key)!.items.push(item);
}
const rows: React.ReactNode[] = [];

View File

@@ -140,7 +140,7 @@ export function InspectionReportModal({
// API: key = "report-item-${itemId}" (예: "report-item-42")
const propFiltered = propWorkItems?.filter(w => !w.id.startsWith('mock-'));
const propHasWorkItems = propFiltered && propFiltered.length > 0;
const propHasInspectionData = propInspectionDataMap && propInspectionDataMap.size > 0;
const propHasInspectionData = propInspectionDataMap != null;
const usePropsSource = propHasWorkItems && propHasInspectionData;
const effectiveWorkItems = usePropsSource ? propFiltered : apiWorkItems ?? undefined;
const effectiveInspectionDataMap = usePropsSource ? propInspectionDataMap : apiInspectionDataMap ?? undefined;

View File

@@ -133,15 +133,15 @@ function transformToWorkerScreenFormat(api: WorkOrderApiItem): WorkOrder {
processInfo = { code: rawProcessCode || 'unknown', name: rawProcessName || '알수없음' };
}
// 아이템을 개소(floor_code/symbol_code)별로 그룹핑
// 아이템을 개소(order_node_id)별로 그룹핑 (floor_code/symbol_code는 표시용)
const nodeMap = new Map<string, { nodeId: number | null; nodeName: string; items: typeof api.items }>();
for (const item of (api.items || [])) {
const nodeId = item.source_order_item?.order_node_id ?? null;
const floorCode = item.source_order_item?.floor_code;
const symbolCode = item.source_order_item?.symbol_code;
const floorLabel = [floorCode, symbolCode].filter(Boolean).join('/');
const floorLabel = [floorCode, symbolCode].filter(v => v && v !== '-').join('/');
const nodeName = floorLabel || item.source_order_item?.node?.name || '미지정';
const key = floorLabel || (nodeId != null ? String(nodeId) : `unassigned-${item.id}`);
const key = nodeId != null ? String(nodeId) : (floorLabel || `unassigned-${item.id}`);
if (!nodeMap.has(key)) {
nodeMap.set(key, { nodeId, nodeName, items: [] });
}

View File

@@ -403,6 +403,8 @@ export default function WorkerScreen() {
// 문서 템플릿 데이터 (document_template 기반 동적 검사용)
const [inspectionTemplateData, setInspectionTemplateData] = useState<InspectionTemplateData | undefined>();
const [inspectionDimensions, setInspectionDimensions] = useState<{ width?: number; height?: number }>({});
// 검사 클릭 시 해당 step name 추적 (하드코딩 '중간검사' 제거용)
const [inspectionStepName, setInspectionStepName] = useState<string>('');
// 중간검사 체크 상태 관리: { [itemId]: boolean }
const [inspectionCheckedMap, setInspectionCheckedMap] = useState<Record<string, boolean>>({});
@@ -692,7 +694,7 @@ export default function WorkerScreen() {
workOrderId: selectedOrder.id,
itemNo: index + 1,
itemCode: selectedOrder.orderNo || '-',
itemName: `${group.nodeName} : ${itemSummary}`,
itemName: itemSummary,
floor: (opts.floor as string) || '-',
code: (opts.code as string) || '-',
width: (opts.width as number) || 0,
@@ -811,9 +813,12 @@ export default function WorkerScreen() {
const match = workItems.find((w) => w.apiItemId === apiItem.item_id);
if (match) {
next.set(match.id, apiItem.inspection_data as unknown as InspectionData);
// 중간검사 step 완료 처리
const stepKey = match.id.replace('-node-', '-') + '-중간검사';
completionUpdates[stepKey] = true;
// 검사 step 완료 처리 (실제 step name 사용)
const inspStep = match.steps.find((s) => s.isInspection || s.needsInspection);
if (inspStep) {
const stepKey = `${match.id.replace('-node-', '-')}-${inspStep.name}`;
completionUpdates[stepKey] = true;
}
}
}
return next;
@@ -901,10 +906,16 @@ export default function WorkerScreen() {
// ===== 핸들러 =====
// 중간검사 버튼 클릭 핸들러 - 템플릿 로드 후 모달 열기
const handleInspectionClick = useCallback(async (itemId: string) => {
const handleInspectionClick = useCallback(async (itemId: string, stepName?: string) => {
// 해당 아이템 찾기
const item = workItems.find((w) => w.id === itemId);
if (item) {
// 클릭된 검사 step name 저장 (stepName 미전달 시 item.steps에서 검사 step 탐색)
const resolvedStepName = stepName
|| item.steps.find((s) => s.isInspection || s.needsInspection)?.name
|| '';
setInspectionStepName(resolvedStepName);
// 합성 WorkOrder 생성
const syntheticOrder: WorkOrder = {
id: item.id,
@@ -988,10 +999,10 @@ export default function WorkerScreen() {
}
} else if (step.connectionType === '팝업' && step.connectionTarget === '중간검사') {
// 연결정보: 팝업 + 중간검사 → 중간검사 모달 열기
handleInspectionClick(itemId);
handleInspectionClick(itemId, step.name);
} else if (step.needsInspection || step.isInspection) {
// 검사 단계 (processListCache 설정 또는 하드코딩 폴백) → 중간검사 모달 열기
handleInspectionClick(itemId);
handleInspectionClick(itemId, step.name);
} else if (step.completionType === 'click_complete' && step.stepProgressId) {
// 클릭 시 완료 → 서버 토글 API 호출
const workItem = workItems.find((item) => item.id === itemId);
@@ -1256,10 +1267,14 @@ export default function WorkerScreen() {
}
}, [getTargetOrder]);
// 중간검사 완료 핸들러 (API 저장 + 메모리 업데이트 + 공정 단계 완료 처리)
// 검사 완료 핸들러 (API 저장 + 메모리 업데이트 + 공정 단계 완료 처리)
const handleInspectionComplete = useCallback(async (data: InspectionData) => {
if (!selectedOrder) return;
// stepCompletionMap 키 생성 헬퍼 (enrichStep 형식 일치: -node- 제거 + 실제 step name)
const buildStepKey = (stepName: string) =>
`${selectedOrder.id.replace('-node-', '-')}-${stepName}`;
// 메모리에 즉시 반영
setInspectionDataMap((prev) => {
const next = new Map(prev);
@@ -1279,7 +1294,7 @@ export default function WorkerScreen() {
data as unknown as Record<string, unknown>
);
if (result.success) {
toast.success('중간검사가 저장되었습니다.');
toast.success('검사가 저장되었습니다.');
} else {
toast.error(result.error || '검사 데이터 저장에 실패했습니다.');
}
@@ -1291,39 +1306,38 @@ export default function WorkerScreen() {
// Document 동기화 실패는 무시 (template 미연결 시 404 가능)
}
// 3. completionType='inspection_complete'인 단계 자동 완료 처리
const inspectionStep = targetItem.steps.find(
(s) => (s.completionType === 'inspection_complete') || s.needsInspection || s.isInspection
);
// 3. 검사 단계 자동 완료 처리 (inspectionStepName 우선, 없으면 steps에서 탐색)
const stepName = inspectionStepName
|| targetItem.steps.find((s) => (s.completionType === 'inspection_complete') || s.needsInspection || s.isInspection)?.name
|| '';
const inspectionStep = stepName
? targetItem.steps.find((s) => s.name === stepName)
: undefined;
if (inspectionStep?.stepProgressId) {
// 서버에 단계 완료 토글
try {
const toggleResult = await toggleStepProgress(targetItem.workOrderId, inspectionStep.stepProgressId);
if (toggleResult.success) {
// 로컬 상태도 동기화
const stepKey = `${selectedOrder.id}-${inspectionStep.name}`;
setStepCompletionMap((prev) => ({ ...prev, [stepKey]: true }));
setStepCompletionMap((prev) => ({ ...prev, [buildStepKey(stepName)]: true }));
}
} catch {
// 단계 완료 실패 시 로컬만 업데이트
const stepKey = `${selectedOrder.id}-${inspectionStep.name}`;
setStepCompletionMap((prev) => ({ ...prev, [stepKey]: true }));
setStepCompletionMap((prev) => ({ ...prev, [buildStepKey(stepName)]: true }));
}
} else {
// stepProgressId 없으면 로컬만 완료 처리 (목업 호환)
const stepKey = selectedOrder.id.replace('-node-', '-') + '-중간검사';
setStepCompletionMap((prev) => ({ ...prev, [stepKey]: true }));
} else if (stepName) {
// stepProgressId 없으면 로컬만 완료 처리
setStepCompletionMap((prev) => ({ ...prev, [buildStepKey(stepName)]: true }));
}
} catch {
toast.error('검사 데이터 저장 중 오류가 발생했습니다.');
}
} else {
} else if (inspectionStepName) {
// 목업 데이터는 메모리만 저장 + 로컬 완료 처리
const stepKey = selectedOrder.id.replace('-node-', '-') + '-중간검사';
setStepCompletionMap((prev) => ({ ...prev, [stepKey]: true }));
setStepCompletionMap((prev) => ({ ...prev, [buildStepKey(inspectionStepName)]: true }));
toast.success('중간검사가 완료되었습니다.');
}
}, [selectedOrder, workItems, getInspectionProcessType]);
}, [selectedOrder, workItems, getInspectionProcessType, inspectionStepName]);
// ===== 재공품 감지 =====
const hasWipItems = useMemo(() => {