- 길이 (1,000)
+ 길이 ({formatDimension(workItemDimensions?.width)})
handleNumberChange('length', e.target.value)}
className="h-11 rounded-lg border-gray-300"
diff --git a/src/components/production/WorkerScreen/actions.ts b/src/components/production/WorkerScreen/actions.ts
index aba72e41..7294153d 100644
--- a/src/components/production/WorkerScreen/actions.ts
+++ b/src/components/production/WorkerScreen/actions.ts
@@ -23,6 +23,10 @@ interface WorkOrderApiItem {
process_name: string;
process_code: string;
department?: string | null;
+ options?: {
+ needs_inspection?: boolean;
+ needs_work_log?: boolean;
+ } | null;
};
/** @deprecated process_id + process relation 사용 */
process_type?: 'screen' | 'slat' | 'bending';
@@ -151,7 +155,7 @@ function transformToWorkerScreenFormat(api: WorkOrderApiItem): WorkOrder {
projectName: api.project_name || '-',
assignees: api.assignee ? [api.assignee.name] : [],
quantity: totalQuantity,
- shutterCount: api.sales_order?.root_nodes_count || 0,
+ shutterCount: nodeGroups.length || api.sales_order?.root_nodes_count || 0,
dueDate,
priority: 5, // 기본 우선순위
status: mapApiStatus(api.status),
@@ -161,6 +165,10 @@ function transformToWorkerScreenFormat(api: WorkOrderApiItem): WorkOrder {
instruction: api.memo || undefined,
salesOrderNo: api.sales_order?.order_no || undefined,
createdAt: api.created_at,
+ processOptions: {
+ needsInspection: api.process?.options?.needs_inspection ?? false,
+ needsWorkLog: api.process?.options?.needs_work_log ?? false,
+ },
nodeGroups,
};
}
@@ -419,18 +427,38 @@ export async function getWorkOrderDetail(
if (stepProgressList.length > 0) {
steps = stepProgressList
.filter((sp: { work_order_item_id: number | null }) => !sp.work_order_item_id || sp.work_order_item_id === item.id)
- .map((sp: { id: number; process_step: { step_name: string; step_code: string } | null; status: string }) => ({
+ .map((sp: {
+ id: number;
+ process_step: {
+ step_name: string; step_code: string;
+ needs_inspection?: boolean; connection_type?: string; completion_type?: string;
+ } | null;
+ status: string;
+ }) => ({
id: String(sp.id),
name: sp.process_step?.step_name || '',
isMaterialInput: (sp.process_step?.step_code || '').includes('MAT') || (sp.process_step?.step_name || '').includes('자재투입'),
+ isInspection: sp.process_step?.needs_inspection || false,
isCompleted: sp.status === 'completed',
+ stepProgressId: sp.id,
+ needsInspection: sp.process_step?.needs_inspection || false,
+ connectionType: sp.process_step?.connection_type || undefined,
+ connectionTarget: undefined, // step_progress API에 미포함, processListCache에서 보완
+ completionType: sp.process_step?.completion_type || undefined,
}));
} else {
- steps = processSteps.map((ps: { id: number; step_name: string; step_code: string }, si: number) => ({
+ steps = processSteps.map((ps: {
+ id: number; step_name: string; step_code: string;
+ needs_inspection?: boolean; connection_type?: string; completion_type?: string;
+ }, si: number) => ({
id: `${item.id}-step-${si}`,
name: ps.step_name,
isMaterialInput: ps.step_code.includes('MAT') || ps.step_name.includes('자재투입'),
+ isInspection: ps.needs_inspection || false,
isCompleted: false,
+ needsInspection: ps.needs_inspection || false,
+ connectionType: ps.connection_type || undefined,
+ completionType: ps.completion_type || undefined,
}));
}
diff --git a/src/components/production/WorkerScreen/index.tsx b/src/components/production/WorkerScreen/index.tsx
index d7f1430a..6ebd25f4 100644
--- a/src/components/production/WorkerScreen/index.tsx
+++ b/src/components/production/WorkerScreen/index.tsx
@@ -33,7 +33,7 @@ import { Button } from '@/components/ui/button';
import { PageLayout } from '@/components/organisms/PageLayout';
import { cn } from '@/lib/utils';
import { toast } from 'sonner';
-import { getMyWorkOrders, completeWorkOrder, saveItemInspection, getWorkOrderInspectionData, saveInspectionDocument, getInspectionTemplate } from './actions';
+import { getMyWorkOrders, completeWorkOrder, saveItemInspection, getWorkOrderInspectionData, saveInspectionDocument, getInspectionTemplate, toggleStepProgress } from './actions';
import type { InspectionTemplateData } from './types';
import { getProcessList } from '@/components/process-management/actions';
import type { InspectionSetting, Process } from '@/types/process';
@@ -554,11 +554,35 @@ export default function WorkerScreen() {
}, [filteredWorkOrders]);
// ===== 선택된 작업지시의 개소별 WorkItemData 변환 + 목업 =====
+
+ // 현재 활성 공정의 단계 설정 (processListCache 기반)
+ const activeProcessSteps = useMemo(() => {
+ const process = processListCache.find((p) => p.id === activeTab);
+ return process?.steps || [];
+ }, [activeTab, processListCache]);
+
const workItems: WorkItemData[] = useMemo(() => {
const selectedOrder = filteredWorkOrders.find((wo) => wo.id === selectedSidebarOrderId);
const stepsKey = (activeProcessTabKey === 'bending' && bendingSubMode === 'wip') ? 'bending_wip' : activeProcessTabKey;
const stepsTemplate = PROCESS_STEPS[stepsKey];
+ // PROCESS_STEPS 폴백 step에 processListCache 설정 매칭하는 헬퍼
+ const enrichStep = (st: { name: string; isMaterialInput: boolean; isInspection?: boolean }, stepId: string, stepKey: string) => {
+ // 단계명으로 processListCache의 단계 설정 매칭
+ const matched = activeProcessSteps.find((ps) => ps.stepName === st.name);
+ return {
+ id: stepId,
+ name: st.name,
+ isMaterialInput: st.isMaterialInput,
+ isInspection: matched ? matched.needsInspection : (st.isInspection || false),
+ isCompleted: stepCompletionMap[stepKey] || false,
+ needsInspection: matched?.needsInspection,
+ connectionType: matched?.connectionType,
+ connectionTarget: matched?.connectionTarget,
+ completionType: matched?.completionType,
+ };
+ };
+
const apiItems: WorkItemData[] = [];
if (selectedOrder && selectedOrder.nodeGroups && selectedOrder.nodeGroups.length > 0) {
@@ -567,13 +591,7 @@ export default function WorkerScreen() {
const nodeKey = group.nodeId != null ? String(group.nodeId) : `unassigned-${index}`;
const steps: WorkStepData[] = stepsTemplate.map((st, si) => {
const stepKey = `${selectedOrder.id}-${nodeKey}-${st.name}`;
- return {
- id: `${selectedOrder.id}-${nodeKey}-step-${si}`,
- name: st.name,
- isMaterialInput: st.isMaterialInput,
- isInspection: st.isInspection,
- isCompleted: stepCompletionMap[stepKey] || false,
- };
+ return enrichStep(st, `${selectedOrder.id}-${nodeKey}-step-${si}`, stepKey);
});
// 개소 내 아이템 이름 요약
@@ -639,13 +657,7 @@ export default function WorkerScreen() {
// nodeGroups가 없는 경우 폴백 (단일 항목)
const steps: WorkStepData[] = stepsTemplate.map((st, si) => {
const stepKey = `${selectedOrder.id}-${st.name}`;
- return {
- id: `${selectedOrder.id}-step-${si}`,
- name: st.name,
- isMaterialInput: st.isMaterialInput,
- isInspection: st.isInspection,
- isCompleted: stepCompletionMap[stepKey] || false,
- };
+ return enrichStep(st, `${selectedOrder.id}-step-${si}`, stepKey);
});
apiItems.push({
id: selectedOrder.id,
@@ -685,7 +697,7 @@ export default function WorkerScreen() {
}));
return [...apiItems, ...mockItems];
- }, [filteredWorkOrders, selectedSidebarOrderId, activeProcessTabKey, stepCompletionMap, bendingSubMode, slatSubMode]);
+ }, [filteredWorkOrders, selectedSidebarOrderId, activeProcessTabKey, stepCompletionMap, bendingSubMode, slatSubMode, activeProcessSteps]);
// ===== 수주 정보 (사이드바 선택 항목 기반) =====
const orderInfo = useMemo(() => {
@@ -733,6 +745,53 @@ export default function WorkerScreen() {
// ===== 핸들러 =====
+ // 중간검사 버튼 클릭 핸들러 - 템플릿 로드 후 모달 열기
+ const handleInspectionClick = useCallback(async (itemId: string) => {
+ // 해당 아이템 찾기
+ const item = workItems.find((w) => w.id === itemId);
+ if (item) {
+ // 합성 WorkOrder 생성
+ const syntheticOrder: WorkOrder = {
+ id: item.id,
+ orderNo: item.itemCode,
+ productName: item.itemName,
+ processCode: item.processType,
+ processName: PROCESS_TAB_LABELS[item.processType],
+ client: '-',
+ projectName: '-',
+ assignees: [],
+ quantity: item.quantity,
+ shutterCount: 0,
+ dueDate: '',
+ priority: 5,
+ status: 'waiting',
+ isUrgent: false,
+ isDelayed: false,
+ createdAt: '',
+ };
+ setSelectedOrder(syntheticOrder);
+ setInspectionDimensions({ width: item.width, height: item.height });
+
+ // 실제 API 아이템인 경우 검사 템플릿 로딩 시도
+ if (item.workOrderId && !item.id.startsWith('mock-')) {
+ try {
+ const tplResult = await getInspectionTemplate(item.workOrderId);
+ if (tplResult.success && tplResult.data?.has_template) {
+ setInspectionTemplateData(tplResult.data);
+ } else {
+ setInspectionTemplateData(undefined);
+ }
+ } catch {
+ setInspectionTemplateData(undefined);
+ }
+ } else {
+ setInspectionTemplateData(undefined);
+ }
+
+ setIsInspectionInputModalOpen(true);
+ }
+ }, [workItems]);
+
// pill 클릭 핸들러
const handleStepClick = useCallback(
(itemId: string, step: WorkStepData) => {
@@ -768,6 +827,12 @@ export default function WorkerScreen() {
setIsMaterialModalOpen(true);
}
}
+ } else if (step.connectionType === '팝업' && step.connectionTarget === '중간검사') {
+ // 연결정보: 팝업 + 중간검사 → 중간검사 모달 열기
+ handleInspectionClick(itemId);
+ } else if (step.needsInspection || step.isInspection) {
+ // 검사 단계 (processListCache 설정 또는 하드코딩 폴백) → 중간검사 모달 열기
+ handleInspectionClick(itemId);
} else {
// 기타 → 완료/미완료 토글
const stepKey = `${itemId}-${step.name}`;
@@ -777,7 +842,7 @@ export default function WorkerScreen() {
}));
}
},
- [workOrders, workItems]
+ [workOrders, workItems, handleInspectionClick]
);
// 자재 수정 핸들러
@@ -891,53 +956,6 @@ export default function WorkerScreen() {
};
}, [filteredWorkOrders, workItems]);
- // 중간검사 버튼 클릭 핸들러 - 템플릿 로드 후 모달 열기
- const handleInspectionClick = useCallback(async (itemId: string) => {
- // 해당 아이템 찾기
- const item = workItems.find((w) => w.id === itemId);
- if (item) {
- // 합성 WorkOrder 생성
- const syntheticOrder: WorkOrder = {
- id: item.id,
- orderNo: item.itemCode,
- productName: item.itemName,
- processCode: item.processType,
- processName: PROCESS_TAB_LABELS[item.processType],
- client: '-',
- projectName: '-',
- assignees: [],
- quantity: item.quantity,
- shutterCount: 0,
- dueDate: '',
- priority: 5,
- status: 'waiting',
- isUrgent: false,
- isDelayed: false,
- createdAt: '',
- };
- setSelectedOrder(syntheticOrder);
- setInspectionDimensions({ width: item.width, height: item.height });
-
- // 실제 API 아이템인 경우 검사 템플릿 로딩 시도
- if (item.workOrderId && !item.id.startsWith('mock-')) {
- try {
- const tplResult = await getInspectionTemplate(item.workOrderId);
- if (tplResult.success && tplResult.data?.has_template) {
- setInspectionTemplateData(tplResult.data);
- } else {
- setInspectionTemplateData(undefined);
- }
- } catch {
- setInspectionTemplateData(undefined);
- }
- } else {
- setInspectionTemplateData(undefined);
- }
-
- setIsInspectionInputModalOpen(true);
- }
- }, [workItems]);
-
// 현재 공정에 맞는 중간검사 타입 결정
const getInspectionProcessType = useCallback((): InspectionProcessType => {
if (activeProcessTabKey === 'bending' && bendingSubMode === 'wip') {
@@ -970,7 +988,7 @@ export default function WorkerScreen() {
}
}, [getTargetOrder]);
- // 중간검사 완료 핸들러 (API 저장 + 메모리 업데이트)
+ // 중간검사 완료 핸들러 (API 저장 + 메모리 업데이트 + 공정 단계 완료 처리)
const handleInspectionComplete = useCallback(async (data: InspectionData) => {
if (!selectedOrder) return;
@@ -981,10 +999,6 @@ export default function WorkerScreen() {
return next;
});
- // 중간검사 step 완료 처리
- const stepKey = selectedOrder.id.replace('-node-', '-') + '-중간검사';
- setStepCompletionMap((prev) => ({ ...prev, [stepKey]: true }));
-
// 실제 API item인 경우 서버에 저장
const targetItem = workItems.find((w) => w.id === selectedOrder.id);
if (targetItem?.apiItemId && targetItem?.workOrderId) {
@@ -1010,11 +1024,37 @@ export default function WorkerScreen() {
} catch {
// Document 저장 실패는 무시 (template 미연결 시 404 가능)
}
+
+ // 3. completionType='검사완료 시 완료'인 단계 자동 완료 처리
+ const inspectionStep = targetItem.steps.find(
+ (s) => (s.completionType === '검사완료 시 완료') || s.needsInspection || s.isInspection
+ );
+ 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 }));
+ }
+ } catch {
+ // 단계 완료 실패 시 로컬만 업데이트
+ const stepKey = `${selectedOrder.id}-${inspectionStep.name}`;
+ setStepCompletionMap((prev) => ({ ...prev, [stepKey]: true }));
+ }
+ } else {
+ // stepProgressId 없으면 로컬만 완료 처리 (목업 호환)
+ const stepKey = selectedOrder.id.replace('-node-', '-') + '-중간검사';
+ setStepCompletionMap((prev) => ({ ...prev, [stepKey]: true }));
+ }
} catch {
toast.error('검사 데이터 저장 중 오류가 발생했습니다.');
}
} else {
- // 목업 데이터는 메모리만 저장
+ // 목업 데이터는 메모리만 저장 + 로컬 완료 처리
+ const stepKey = selectedOrder.id.replace('-node-', '-') + '-중간검사';
+ setStepCompletionMap((prev) => ({ ...prev, [stepKey]: true }));
toast.success('중간검사가 완료되었습니다.');
}
}, [selectedOrder, workItems, getInspectionProcessType]);
diff --git a/src/components/production/WorkerScreen/types.ts b/src/components/production/WorkerScreen/types.ts
index c0ed0a34..4e53aee8 100644
--- a/src/components/production/WorkerScreen/types.ts
+++ b/src/components/production/WorkerScreen/types.ts
@@ -112,6 +112,12 @@ export interface WorkStepData {
isMaterialInput: boolean; // 자재투입 단계 여부
isInspection?: boolean; // 중간검사 단계 여부
isCompleted: boolean; // 완료 여부
+ // 공정 단계 설정 연동 (공정관리 페이지의 단계 설정)
+ stepProgressId?: number; // work_order_step_progress.id (서버 완료 토글용)
+ needsInspection?: boolean; // 검사여부 (ProcessStep.needs_inspection)
+ connectionType?: string; // 연결 유형: '팝업' | '없음'
+ connectionTarget?: string; // 도달: '중간검사' 등
+ completionType?: string; // 완료 유형: '검사완료 시 완료' | '클릭 시 완료' | '선택 완료 시 완료'
}
// ===== 자재 투입 목록 항목 =====
@@ -244,6 +250,8 @@ export interface InspectionTemplateFormat {
sections: {
id: number;
name: string;
+ title?: string;
+ image_path?: string | null;
sort_order: number;
items: InspectionTemplateSectionItem[];
}[];
@@ -260,10 +268,10 @@ export interface InspectionTemplateFormat {
columns: {
id: number;
label: string;
- input_type: string;
- options: Record | null;
+ column_type: string;
+ sub_labels: string[] | null;
+ group_name: string | null;
width: string | null;
- is_required: boolean;
sort_order: number;
}[];
approval_lines: {
@@ -276,12 +284,10 @@ export interface InspectionTemplateFormat {
}[];
basic_fields: {
id: number;
- field_key: string;
+ field_key: string | null;
label: string;
- input_type: string;
- options: Record | null;
+ field_type: string;
default_value: string | null;
- is_required: boolean;
sort_order: number;
}[];
}
diff --git a/src/types/process.ts b/src/types/process.ts
index 87e56e2d..7a9e2198 100644
--- a/src/types/process.ts
+++ b/src/types/process.ts
@@ -59,13 +59,16 @@ export interface Process {
department: string; // 담당부서
workLogTemplate?: string; // 작업일지 양식 (레거시 string)
- // 중간검사/작업일지 설정 (Process 레벨)
+ // 검사/양식 FK
documentTemplateId?: number; // 중간검사 양식 ID
documentTemplateName?: string; // 중간검사 양식명 (표시용)
- needsWorkLog: boolean; // 작업일지 여부
workLogTemplateId?: number; // 작업일지 양식 ID
workLogTemplateName?: string; // 작업일지 양식명 (표시용)
+ // 공정 설정 (options JSON)
+ needsInspection: boolean; // 중간검사 여부
+ needsWorkLog: boolean; // 작업일지 여부
+
// 자동 분류 규칙
classificationRules: ClassificationRule[];
@@ -107,8 +110,9 @@ export interface ProcessFormData {
useProductionDate?: boolean;
workLogTemplate?: string;
documentTemplateId?: number;
- needsWorkLog: boolean;
workLogTemplateId?: number;
+ needsInspection: boolean;
+ needsWorkLog: boolean;
classificationRules: ClassificationRuleInput[];
requiredWorkers: number;
equipmentInfo?: string;