feat: 견적확정 밸리데이션, 수주등록 개소그룹, 작업지시 개선
- 견적확정 시 업체명/현장명/담당자/연락처 프론트 밸리데이션 추가 - 견적확정 후 수주등록 버튼 동적 전환 - 수주등록 품목 개소별(floor+code) 그룹핑 수정 - 작업지시 상세 quantity 문자열→숫자 변환 (formatQuantity) - 작업지시 탭 카운트 초기 로딩 시 전체 표시 (by_process 활용) - 작업지시 상세 개소별/품목별 합산 테이블 추가 - 작업자 화면 API 연동 및 목업 데이터 분리 - 입고관리 완료건 수정, 재고현황 개선
This commit is contained in:
@@ -232,8 +232,8 @@ export function MaterialInputModal({
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{materials.map((material) => (
|
||||
<TableRow key={material.id}>
|
||||
{materials.map((material, index) => (
|
||||
<TableRow key={`mat-${material.id}-${index}`}>
|
||||
<TableCell className="text-center text-sm">
|
||||
{material.materialCode}
|
||||
</TableCell>
|
||||
|
||||
@@ -76,7 +76,7 @@ export function WorkItemCard({
|
||||
{item.itemNo}
|
||||
</span>
|
||||
<span className="text-base font-bold text-gray-900">
|
||||
{item.itemCode} ({item.itemName})
|
||||
{item.itemCode} - {item.itemName}
|
||||
</span>
|
||||
</div>
|
||||
{!item.isWip && (
|
||||
|
||||
131
src/components/production/WorkerScreen/WorkOrderListPanel.tsx
Normal file
131
src/components/production/WorkerScreen/WorkOrderListPanel.tsx
Normal file
@@ -0,0 +1,131 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 작업지시서 리스트 패널 (좌측)
|
||||
*
|
||||
* 마스터-디테일 레이아웃의 좌측 패널.
|
||||
* 공정별 필터링된 작업지시서 목록을 표시하고 선택 기능 제공.
|
||||
*/
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
import { AlertTriangle, Package } from 'lucide-react';
|
||||
import type { WorkOrder } from '../ProductionDashboard/types';
|
||||
|
||||
interface WorkOrderListPanelProps {
|
||||
workOrders: WorkOrder[];
|
||||
selectedId: string | null;
|
||||
onSelect: (id: string) => void;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
export function WorkOrderListPanel({
|
||||
workOrders,
|
||||
selectedId,
|
||||
onSelect,
|
||||
isLoading,
|
||||
}: WorkOrderListPanelProps) {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="p-2 space-y-1.5">
|
||||
{Array.from({ length: 5 }).map((_, i) => (
|
||||
<div key={i} className="h-[84px] rounded-lg bg-gray-100 animate-pulse" />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (workOrders.length === 0) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-full text-muted-foreground p-6">
|
||||
<Package className="h-10 w-10 mb-2 opacity-40" />
|
||||
<p className="text-sm">배정된 작업이 없습니다.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="overflow-y-auto h-full p-2 space-y-1.5">
|
||||
{workOrders.map((order, index) => (
|
||||
<button
|
||||
key={order.id}
|
||||
onClick={() => onSelect(order.id)}
|
||||
className={cn(
|
||||
'w-full text-left p-3 rounded-lg border transition-all duration-150',
|
||||
selectedId === order.id
|
||||
? 'bg-primary/5 border-primary/30 shadow-sm'
|
||||
: 'bg-white border-gray-100 hover:bg-gray-50 hover:border-gray-200'
|
||||
)}
|
||||
>
|
||||
{/* 헤더: 번호 + 작업지시번호 + 상태 */}
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<span className="flex items-center justify-center w-6 h-6 rounded-full bg-emerald-500 text-white text-xs font-bold shrink-0">
|
||||
{index + 1}
|
||||
</span>
|
||||
<span className="text-sm font-semibold text-gray-900 truncate">
|
||||
{order.orderNo}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 shrink-0 ml-2">
|
||||
{order.isUrgent && (
|
||||
<AlertTriangle className="h-3.5 w-3.5 text-red-500" />
|
||||
)}
|
||||
<StatusBadge status={order.status} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 품목명 */}
|
||||
<p className="text-xs text-gray-600 truncate ml-8">{order.productName}</p>
|
||||
|
||||
{/* 현장명 + 수량 */}
|
||||
<div className="flex items-center justify-between mt-1.5 ml-8">
|
||||
<span className="text-xs text-gray-400 truncate">
|
||||
{order.projectName || order.client}
|
||||
</span>
|
||||
<span className="text-xs font-medium text-gray-500 shrink-0">
|
||||
{order.quantity}EA
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 납기일 */}
|
||||
{order.dueDate && (
|
||||
<div className="mt-1 ml-8">
|
||||
<span
|
||||
className={cn(
|
||||
'text-[11px]',
|
||||
order.isDelayed
|
||||
? 'text-red-500 font-medium'
|
||||
: 'text-gray-400'
|
||||
)}
|
||||
>
|
||||
납기: {new Date(order.dueDate).toLocaleDateString('ko-KR')}
|
||||
{order.isDelayed &&
|
||||
order.delayDays &&
|
||||
` (${order.delayDays}일 지연)`}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function StatusBadge({ status }: { status: string }) {
|
||||
const config: Record<string, { label: string; className: string }> = {
|
||||
waiting: { label: '대기', className: 'bg-gray-100 text-gray-600' },
|
||||
inProgress: { label: '작업중', className: 'bg-blue-100 text-blue-700' },
|
||||
completed: { label: '완료', className: 'bg-green-100 text-green-700' },
|
||||
};
|
||||
const { label, className } = config[status] || config.waiting;
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
'text-[10px] font-medium px-1.5 py-0.5 rounded',
|
||||
className
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* 작업자 화면 서버 액션
|
||||
* API 연동 완료 (2025-12-26)
|
||||
* API 연동 (2025-12-26 초기, 2026-02-06 options + step-progress 확장)
|
||||
*
|
||||
* WorkOrders API를 호출하고 WorkerScreen에 맞는 형식으로 변환
|
||||
*/
|
||||
@@ -11,13 +11,22 @@
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
||||
import type { WorkOrder, WorkOrderStatus } from '../ProductionDashboard/types';
|
||||
import type { WorkItemData, WorkStepData, ProcessTab } from './types';
|
||||
|
||||
// ===== API 타입 =====
|
||||
interface WorkOrderApiItem {
|
||||
id: number;
|
||||
work_order_no: string;
|
||||
project_name: string | null;
|
||||
process_type: 'screen' | 'slat' | 'bending';
|
||||
process_id: number | null;
|
||||
process?: {
|
||||
id: number;
|
||||
process_name: string;
|
||||
process_code: string;
|
||||
department?: string | null;
|
||||
};
|
||||
/** @deprecated process_id + process relation 사용 */
|
||||
process_type?: 'screen' | 'slat' | 'bending';
|
||||
status: 'unassigned' | 'pending' | 'waiting' | 'in_progress' | 'completed' | 'shipped';
|
||||
scheduled_date: string | null;
|
||||
memo: string | null;
|
||||
@@ -26,9 +35,21 @@ interface WorkOrderApiItem {
|
||||
id: number;
|
||||
order_no: string;
|
||||
client?: { id: number; name: string };
|
||||
root_nodes_count?: number;
|
||||
};
|
||||
assignee?: { id: number; name: string };
|
||||
items?: { id: number; item_name: string; quantity: number }[];
|
||||
items?: {
|
||||
id: number;
|
||||
item_name: string;
|
||||
quantity: number;
|
||||
specification?: string | null;
|
||||
options?: Record<string, unknown> | null;
|
||||
source_order_item?: {
|
||||
id: number;
|
||||
order_node_id: number | null;
|
||||
node?: { id: number; name: string; code: string } | null;
|
||||
} | null;
|
||||
}[];
|
||||
}
|
||||
|
||||
// ===== 상태 변환 =====
|
||||
@@ -46,7 +67,7 @@ function mapApiStatus(status: WorkOrderApiItem['status']): WorkOrderStatus {
|
||||
|
||||
// ===== API → WorkOrder 변환 =====
|
||||
function transformToWorkerScreenFormat(api: WorkOrderApiItem): WorkOrder {
|
||||
const totalQuantity = (api.items || []).reduce((sum, item) => sum + item.quantity, 0);
|
||||
const totalQuantity = (api.items || []).reduce((sum, item) => sum + Number(item.quantity), 0);
|
||||
const productName = api.items?.[0]?.item_name || '-';
|
||||
|
||||
// 납기일 계산 (지연 여부)
|
||||
@@ -59,13 +80,62 @@ function transformToWorkerScreenFormat(api: WorkOrderApiItem): WorkOrder {
|
||||
? Math.ceil((today.getTime() - due.getTime()) / (1000 * 60 * 60 * 24))
|
||||
: undefined;
|
||||
|
||||
// process_type → processCode/processName 매핑
|
||||
const processTypeMap: Record<string, { code: string; name: string }> = {
|
||||
screen: { code: 'screen', name: '스크린' },
|
||||
slat: { code: 'slat', name: '슬랫' },
|
||||
bending: { code: 'bending', name: '절곡' },
|
||||
// process relation → processCode/processName 매핑
|
||||
// 신규: process relation 사용, 폴백: deprecated process_type 필드
|
||||
const rawProcessName = api.process?.process_name || '';
|
||||
const rawProcessCode = api.process?.process_code || '';
|
||||
|
||||
// 한글 공정명 → 탭 키 매핑 (필터링에 사용)
|
||||
const nameToTab: Record<string, { code: string; name: string }> = {
|
||||
'스크린': { code: 'screen', name: '스크린' },
|
||||
'슬랫': { code: 'slat', name: '슬랫' },
|
||||
'절곡': { code: 'bending', name: '절곡' },
|
||||
};
|
||||
const processInfo = processTypeMap[api.process_type] || { code: api.process_type, name: api.process_type };
|
||||
|
||||
// 1차: process relation의 process_name으로 매핑
|
||||
let processInfo = Object.entries(nameToTab).find(
|
||||
([key]) => rawProcessName.includes(key)
|
||||
)?.[1];
|
||||
|
||||
// 2차: deprecated process_type 폴백
|
||||
if (!processInfo && api.process_type) {
|
||||
const legacyMap: Record<string, { code: string; name: string }> = {
|
||||
screen: { code: 'screen', name: '스크린' },
|
||||
slat: { code: 'slat', name: '슬랫' },
|
||||
bending: { code: 'bending', name: '절곡' },
|
||||
};
|
||||
processInfo = legacyMap[api.process_type];
|
||||
}
|
||||
|
||||
// 3차: 그래도 없으면 원시값 사용
|
||||
if (!processInfo) {
|
||||
processInfo = { code: rawProcessCode || 'unknown', name: rawProcessName || '알수없음' };
|
||||
}
|
||||
|
||||
// 아이템을 개소(node)별로 그룹핑
|
||||
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 nodeName = item.source_order_item?.node?.name || '미지정';
|
||||
const key = nodeId != null ? String(nodeId) : 'unassigned';
|
||||
if (!nodeMap.has(key)) {
|
||||
nodeMap.set(key, { nodeId, nodeName, items: [] });
|
||||
}
|
||||
nodeMap.get(key)!.items!.push(item);
|
||||
}
|
||||
|
||||
const nodeGroups = Array.from(nodeMap.values()).map((g) => ({
|
||||
nodeId: g.nodeId,
|
||||
nodeName: g.nodeName,
|
||||
items: (g.items || []).map((it) => ({
|
||||
id: it.id,
|
||||
itemName: it.item_name,
|
||||
quantity: Number(it.quantity),
|
||||
specification: it.specification,
|
||||
options: it.options,
|
||||
})),
|
||||
totalQuantity: (g.items || []).reduce((sum, it) => sum + Number(it.quantity), 0),
|
||||
}));
|
||||
|
||||
return {
|
||||
id: String(api.id),
|
||||
@@ -77,6 +147,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,
|
||||
dueDate,
|
||||
priority: 5, // 기본 우선순위
|
||||
status: mapApiStatus(api.status),
|
||||
@@ -84,7 +155,9 @@ function transformToWorkerScreenFormat(api: WorkOrderApiItem): WorkOrder {
|
||||
isDelayed,
|
||||
delayDays,
|
||||
instruction: api.memo || undefined,
|
||||
salesOrderNo: api.sales_order?.order_no || undefined,
|
||||
createdAt: api.created_at,
|
||||
nodeGroups,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -95,8 +168,8 @@ export async function getMyWorkOrders(): Promise<{
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
// 작업 대기 + 작업중 상태만 조회 (완료 제외)
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders?per_page=100&assigned_to_me=1`;
|
||||
// 작업자 화면용 캐스케이드 필터: 개인 배정 → 부서 → 전체
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders?per_page=100&worker_screen=1`;
|
||||
|
||||
console.log('[WorkerScreenActions] GET my work orders:', url);
|
||||
|
||||
@@ -530,4 +603,216 @@ export async function requestInspection(
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 공정 단계 진행 현황 조회 =====
|
||||
export interface StepProgressItem {
|
||||
id: number;
|
||||
process_step_id: number;
|
||||
step_code: string;
|
||||
step_name: string;
|
||||
sort_order: number;
|
||||
needs_inspection: boolean;
|
||||
connection_type: string | null;
|
||||
completion_type: string | null;
|
||||
status: string;
|
||||
is_completed: boolean;
|
||||
completed_at: string | null;
|
||||
completed_by: number | null;
|
||||
}
|
||||
|
||||
export async function getStepProgress(
|
||||
workOrderId: string
|
||||
): Promise<{
|
||||
success: boolean;
|
||||
data: StepProgressItem[];
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${workOrderId}/step-progress`;
|
||||
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
|
||||
if (error || !response) {
|
||||
return { success: false, data: [], error: error?.message || '네트워크 오류' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, data: [], error: result.message || '단계 진행 조회 실패' };
|
||||
}
|
||||
|
||||
return { success: true, data: result.data || [] };
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[WorkerScreenActions] getStepProgress error:', error);
|
||||
return { success: false, data: [], error: '서버 오류' };
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 공정 단계 완료 토글 =====
|
||||
export async function toggleStepProgress(
|
||||
workOrderId: string,
|
||||
progressId: number
|
||||
): Promise<{ success: boolean; data?: StepProgressItem; error?: string }> {
|
||||
try {
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${workOrderId}/step-progress/${progressId}/toggle`;
|
||||
|
||||
const { response, error } = await serverFetch(url, { method: 'PATCH' });
|
||||
|
||||
if (error || !response) {
|
||||
return { success: false, error: error?.message || '네트워크 오류' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '단계 토글 실패' };
|
||||
}
|
||||
|
||||
return { success: true, data: result.data };
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[WorkerScreenActions] toggleStepProgress error:', error);
|
||||
return { success: false, error: '서버 오류' };
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 작업지시 상세 조회 (items + options 포함) =====
|
||||
export async function getWorkOrderDetail(
|
||||
workOrderId: string
|
||||
): Promise<{
|
||||
success: boolean;
|
||||
data: WorkItemData[];
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${workOrderId}`;
|
||||
|
||||
const { response, error } = await serverFetch(url, { method: 'GET' });
|
||||
|
||||
if (error || !response) {
|
||||
return { success: false, data: [], error: error?.message || '네트워크 오류' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, data: [], error: result.message || '상세 조회 실패' };
|
||||
}
|
||||
|
||||
const wo = result.data;
|
||||
const processName = (wo.process?.process_name || '').toLowerCase();
|
||||
const processType: ProcessTab =
|
||||
processName.includes('스크린') ? 'screen' :
|
||||
processName.includes('슬랫') ? 'slat' :
|
||||
processName.includes('절곡') ? 'bending' : 'screen';
|
||||
|
||||
// items → WorkItemData 변환 (options 파싱)
|
||||
const items: WorkItemData[] = (wo.items || []).map((item: {
|
||||
id: number;
|
||||
item_name: string;
|
||||
specification: string | null;
|
||||
quantity: string;
|
||||
unit: string | null;
|
||||
status: string;
|
||||
sort_order: number;
|
||||
options: Record<string, unknown> | null;
|
||||
}, index: number) => {
|
||||
const opts = item.options || {};
|
||||
|
||||
// steps: stepProgress에서 가져오거나 process.steps에서 생성
|
||||
const stepProgressList = wo.step_progress || [];
|
||||
const processSteps = wo.process?.steps || [];
|
||||
|
||||
let steps: WorkStepData[];
|
||||
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 }) => ({
|
||||
id: String(sp.id),
|
||||
name: sp.process_step?.step_name || '',
|
||||
isMaterialInput: (sp.process_step?.step_code || '').includes('MAT') || (sp.process_step?.step_name || '').includes('자재투입'),
|
||||
isCompleted: sp.status === 'completed',
|
||||
}));
|
||||
} else {
|
||||
steps = processSteps.map((ps: { id: number; step_name: string; step_code: string }, si: number) => ({
|
||||
id: `${item.id}-step-${si}`,
|
||||
name: ps.step_name,
|
||||
isMaterialInput: ps.step_code.includes('MAT') || ps.step_name.includes('자재투입'),
|
||||
isCompleted: false,
|
||||
}));
|
||||
}
|
||||
|
||||
const workItem: WorkItemData = {
|
||||
id: String(item.id),
|
||||
itemNo: index + 1,
|
||||
itemCode: wo.work_order_no || '-',
|
||||
itemName: item.item_name || '-',
|
||||
floor: (opts.floor as string) || '-',
|
||||
code: (opts.code as string) || '-',
|
||||
width: (opts.width as number) || 0,
|
||||
height: (opts.height as number) || 0,
|
||||
quantity: Number(item.quantity) || 0,
|
||||
processType,
|
||||
steps,
|
||||
materialInputs: [],
|
||||
};
|
||||
|
||||
// 공정별 상세 정보 파싱 (options에서)
|
||||
if (opts.cutting_info) {
|
||||
const ci = opts.cutting_info as { width: number; sheets: number };
|
||||
workItem.cuttingInfo = { width: ci.width, sheets: ci.sheets };
|
||||
}
|
||||
if (opts.slat_info) {
|
||||
const si = opts.slat_info as { length: number; slat_count: number; joint_bar: number };
|
||||
workItem.slatInfo = { length: si.length, slatCount: si.slat_count, jointBar: si.joint_bar };
|
||||
}
|
||||
if (opts.bending_info) {
|
||||
const bi = opts.bending_info as {
|
||||
common: { kind: string; type: string; length_quantities: { length: number; quantity: number }[] };
|
||||
detail_parts: { part_name: string; material: string; barcy_info: string }[];
|
||||
};
|
||||
workItem.bendingInfo = {
|
||||
common: {
|
||||
kind: bi.common.kind,
|
||||
type: bi.common.type,
|
||||
lengthQuantities: bi.common.length_quantities || [],
|
||||
},
|
||||
detailParts: (bi.detail_parts || []).map(dp => ({
|
||||
partName: dp.part_name,
|
||||
material: dp.material,
|
||||
barcyInfo: dp.barcy_info,
|
||||
})),
|
||||
};
|
||||
}
|
||||
if (opts.is_wip) {
|
||||
workItem.isWip = true;
|
||||
const wi = opts.wip_info as { specification: string; length_quantity: string; drawing_url?: string } | undefined;
|
||||
if (wi) {
|
||||
workItem.wipInfo = {
|
||||
specification: wi.specification,
|
||||
lengthQuantity: wi.length_quantity,
|
||||
drawingUrl: wi.drawing_url,
|
||||
};
|
||||
}
|
||||
}
|
||||
if (opts.is_joint_bar) {
|
||||
workItem.isJointBar = true;
|
||||
const jb = opts.slat_joint_bar_info as { specification: string; length: number; quantity: number } | undefined;
|
||||
if (jb) {
|
||||
workItem.slatJointBarInfo = jb;
|
||||
}
|
||||
}
|
||||
|
||||
return workItem;
|
||||
});
|
||||
|
||||
return { success: true, data: items };
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[WorkerScreenActions] getWorkOrderDetail error:', error);
|
||||
return { success: false, data: [], error: '서버 오류' };
|
||||
}
|
||||
}
|
||||
@@ -303,7 +303,7 @@ export default function WorkerScreen() {
|
||||
const { sidebarCollapsed } = useMenuStore();
|
||||
const [workOrders, setWorkOrders] = useState<WorkOrder[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [activeTab, setActiveTab] = useState<ProcessTab>('screen');
|
||||
const [activeTab, setActiveTab] = useState<string>('');
|
||||
const [bendingSubMode, setBendingSubMode] = useState<'normal' | 'wip'>('normal');
|
||||
const [slatSubMode, setSlatSubMode] = useState<'normal' | 'jointbar'>('normal');
|
||||
|
||||
@@ -313,7 +313,7 @@ export default function WorkerScreen() {
|
||||
const [productionDate, setProductionDate] = useState('');
|
||||
|
||||
// 좌측 사이드바
|
||||
const [selectedSidebarOrderId, setSelectedSidebarOrderId] = useState<string>('order-1');
|
||||
const [selectedSidebarOrderId, setSelectedSidebarOrderId] = useState<string>('');
|
||||
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
||||
|
||||
// 공정별 step 완료 상태: { [itemId-stepName]: boolean }
|
||||
@@ -406,24 +406,36 @@ export default function WorkerScreen() {
|
||||
fetchProcessList();
|
||||
}, []);
|
||||
|
||||
// 활성 공정 목록 (탭용) - 공정관리에서 등록된 활성 공정만
|
||||
const processTabs = useMemo(() => {
|
||||
return processListCache.filter((p) => p.status === '사용중');
|
||||
}, [processListCache]);
|
||||
|
||||
// 공정 목록 로드 후 첫 번째 공정을 기본 선택
|
||||
useEffect(() => {
|
||||
if (processTabs.length > 0 && !activeTab) {
|
||||
setActiveTab(processTabs[0].id);
|
||||
}
|
||||
}, [processTabs, activeTab]);
|
||||
|
||||
// 선택된 공정의 ProcessTab 키 (mock 데이터 및 기존 로직 호환용)
|
||||
const activeProcessTabKey: ProcessTab = useMemo(() => {
|
||||
const process = processListCache.find((p) => p.id === activeTab);
|
||||
if (!process) return 'screen';
|
||||
const name = process.processName.toLowerCase();
|
||||
if (name.includes('스크린')) return 'screen';
|
||||
if (name.includes('슬랫')) return 'slat';
|
||||
if (name.includes('절곡')) return 'bending';
|
||||
return 'screen';
|
||||
}, [activeTab, processListCache]);
|
||||
|
||||
// activeTab 변경 시 해당 공정의 중간검사 설정 조회
|
||||
useEffect(() => {
|
||||
if (processListCache.length === 0) return;
|
||||
if (processListCache.length === 0 || !activeTab) return;
|
||||
|
||||
// activeTab에 해당하는 공정 찾기
|
||||
const tabToProcessName: Record<ProcessTab, string[]> = {
|
||||
screen: ['스크린', 'screen'],
|
||||
slat: ['슬랫', 'slat'],
|
||||
bending: ['절곡', 'bending'],
|
||||
};
|
||||
|
||||
const matchNames = tabToProcessName[activeTab] || [];
|
||||
const matchedProcess = processListCache.find((p) =>
|
||||
matchNames.some((name) => p.processName.toLowerCase().includes(name.toLowerCase()))
|
||||
);
|
||||
const matchedProcess = processListCache.find((p) => p.id === activeTab);
|
||||
|
||||
if (matchedProcess?.steps) {
|
||||
// 검사 단계에서 inspectionSetting 찾기
|
||||
const inspectionStep = matchedProcess.steps.find(
|
||||
(step) => step.needsInspection && step.inspectionSetting
|
||||
);
|
||||
@@ -435,22 +447,49 @@ export default function WorkerScreen() {
|
||||
|
||||
// ===== 탭별 필터링된 작업 =====
|
||||
const filteredWorkOrders = useMemo(() => {
|
||||
// process_type 기반 필터링
|
||||
const selectedProcess = processListCache.find((p) => p.id === activeTab);
|
||||
if (!selectedProcess) return workOrders;
|
||||
|
||||
const selectedName = selectedProcess.processName.toLowerCase();
|
||||
return workOrders.filter((order) => {
|
||||
// WorkOrder의 processCode/processName으로 매칭
|
||||
const processName = (order.processName || '').toLowerCase();
|
||||
switch (activeTab) {
|
||||
case 'screen':
|
||||
return processName.includes('스크린') || processName === 'screen';
|
||||
case 'slat':
|
||||
return processName.includes('슬랫') || processName === 'slat';
|
||||
case 'bending':
|
||||
return processName.includes('절곡') || processName === 'bending';
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
const orderProcessName = (order.processName || '').toLowerCase();
|
||||
return orderProcessName.includes(selectedName) || selectedName.includes(orderProcessName);
|
||||
});
|
||||
}, [workOrders, activeTab]);
|
||||
}, [workOrders, activeTab, processListCache]);
|
||||
|
||||
// ===== API WorkOrders → SidebarOrder 변환 =====
|
||||
const apiSidebarOrders: SidebarOrder[] = useMemo(() => {
|
||||
return filteredWorkOrders.map((wo) => ({
|
||||
id: wo.id,
|
||||
siteName: wo.projectName || wo.client || '-',
|
||||
date: wo.dueDate || (wo.createdAt ? wo.createdAt.slice(0, 10) : '-'),
|
||||
quantity: wo.quantity,
|
||||
shutterCount: wo.shutterCount || 0,
|
||||
priority: (wo.isUrgent ? 'urgent' : (wo.priority <= 3 ? 'priority' : 'normal')) as SidebarOrder['priority'],
|
||||
}));
|
||||
}, [filteredWorkOrders]);
|
||||
|
||||
// ===== 탭 변경/데이터 로드 시 최상위 우선순위 작업 자동 선택 =====
|
||||
useEffect(() => {
|
||||
if (isLoading) return;
|
||||
|
||||
const allOrders: SidebarOrder[] = [...apiSidebarOrders, ...MOCK_SIDEBAR_ORDERS[activeProcessTabKey]];
|
||||
// 우선순위 순서: urgent → priority → normal
|
||||
for (const group of PRIORITY_GROUPS) {
|
||||
const first = allOrders.find((o) => o.priority === group.key);
|
||||
if (first) {
|
||||
setSelectedSidebarOrderId(first.id);
|
||||
// subType에 따라 서브모드도 설정
|
||||
if (activeProcessTabKey === 'slat') {
|
||||
setSlatSubMode(first.subType === 'jointbar' ? 'jointbar' : 'normal');
|
||||
}
|
||||
if (activeProcessTabKey === 'bending') {
|
||||
setBendingSubMode(first.subType === 'wip' ? 'wip' : 'normal');
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}, [isLoading, apiSidebarOrders, activeProcessTabKey]);
|
||||
|
||||
// ===== 통계 계산 (탭별) =====
|
||||
const stats: WorkerStats = useMemo(() => {
|
||||
@@ -462,46 +501,84 @@ export default function WorkerScreen() {
|
||||
};
|
||||
}, [filteredWorkOrders]);
|
||||
|
||||
// ===== WorkOrder → WorkItemData 변환 + 목업 =====
|
||||
// ===== 선택된 작업지시의 개소별 WorkItemData 변환 + 목업 =====
|
||||
const workItems: WorkItemData[] = useMemo(() => {
|
||||
const apiItems: WorkItemData[] = filteredWorkOrders.map((order, index) => {
|
||||
const stepsKey = (activeTab === 'bending' && bendingSubMode === 'wip') ? 'bending_wip' : activeTab;
|
||||
const stepsTemplate = PROCESS_STEPS[stepsKey];
|
||||
const selectedOrder = filteredWorkOrders.find((wo) => wo.id === selectedSidebarOrderId);
|
||||
const stepsKey = (activeProcessTabKey === 'bending' && bendingSubMode === 'wip') ? 'bending_wip' : activeProcessTabKey;
|
||||
const stepsTemplate = PROCESS_STEPS[stepsKey];
|
||||
|
||||
const apiItems: WorkItemData[] = [];
|
||||
|
||||
if (selectedOrder && selectedOrder.nodeGroups && selectedOrder.nodeGroups.length > 0) {
|
||||
// 개소별로 WorkItemData 생성
|
||||
selectedOrder.nodeGroups.forEach((group, index) => {
|
||||
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,
|
||||
};
|
||||
});
|
||||
|
||||
// 개소 내 아이템 이름 요약
|
||||
const itemNames = group.items.map((it) => it.itemName).filter(Boolean);
|
||||
const itemSummary = itemNames.length > 0 ? itemNames.join(', ') : '-';
|
||||
|
||||
apiItems.push({
|
||||
id: `${selectedOrder.id}-node-${nodeKey}`,
|
||||
itemNo: index + 1,
|
||||
itemCode: selectedOrder.orderNo || '-',
|
||||
itemName: `${group.nodeName} : ${itemSummary}`,
|
||||
floor: '-',
|
||||
code: '-',
|
||||
width: 0,
|
||||
height: 0,
|
||||
quantity: group.totalQuantity,
|
||||
processType: activeProcessTabKey,
|
||||
steps,
|
||||
materialInputs: [],
|
||||
});
|
||||
});
|
||||
} else if (selectedOrder) {
|
||||
// nodeGroups가 없는 경우 폴백 (단일 항목)
|
||||
const steps: WorkStepData[] = stepsTemplate.map((st, si) => {
|
||||
const stepKey = `${order.id}-${st.name}`;
|
||||
const stepKey = `${selectedOrder.id}-${st.name}`;
|
||||
return {
|
||||
id: `${order.id}-step-${si}`,
|
||||
id: `${selectedOrder.id}-step-${si}`,
|
||||
name: st.name,
|
||||
isMaterialInput: st.isMaterialInput,
|
||||
isInspection: st.isInspection,
|
||||
isCompleted: stepCompletionMap[stepKey] || false,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
id: order.id,
|
||||
itemNo: index + 1,
|
||||
itemCode: order.orderNo || '-',
|
||||
itemName: order.productName || '-',
|
||||
apiItems.push({
|
||||
id: selectedOrder.id,
|
||||
itemNo: 1,
|
||||
itemCode: selectedOrder.orderNo || '-',
|
||||
itemName: selectedOrder.productName || '-',
|
||||
floor: '-',
|
||||
code: '-',
|
||||
width: 0,
|
||||
height: 0,
|
||||
quantity: order.quantity || 0,
|
||||
processType: activeTab,
|
||||
quantity: selectedOrder.quantity || 0,
|
||||
processType: activeProcessTabKey,
|
||||
steps,
|
||||
materialInputs: [],
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 목업 데이터 합치기 (API 데이터 뒤에 번호 이어서)
|
||||
// 절곡 탭에서 재공품 서브모드면 WIP 전용 목업 사용
|
||||
// 슬랫 탭에서 조인트바 서브모드면 조인트바 전용 목업 사용
|
||||
const baseMockItems = (activeTab === 'bending' && bendingSubMode === 'wip')
|
||||
const baseMockItems = (activeProcessTabKey === 'bending' && bendingSubMode === 'wip')
|
||||
? MOCK_ITEMS_BENDING_WIP
|
||||
: (activeTab === 'slat' && slatSubMode === 'jointbar')
|
||||
: (activeProcessTabKey === 'slat' && slatSubMode === 'jointbar')
|
||||
? MOCK_ITEMS_SLAT_JOINTBAR
|
||||
: MOCK_ITEMS[activeTab];
|
||||
: MOCK_ITEMS[activeProcessTabKey];
|
||||
const mockItems = baseMockItems.map((item, i) => ({
|
||||
...item,
|
||||
itemNo: apiItems.length + i + 1,
|
||||
@@ -515,22 +592,51 @@ export default function WorkerScreen() {
|
||||
}));
|
||||
|
||||
return [...apiItems, ...mockItems];
|
||||
}, [filteredWorkOrders, activeTab, stepCompletionMap, bendingSubMode, slatSubMode]);
|
||||
}, [filteredWorkOrders, selectedSidebarOrderId, activeProcessTabKey, stepCompletionMap, bendingSubMode, slatSubMode]);
|
||||
|
||||
// ===== 수주 정보 (첫 번째 작업 기반 표시) =====
|
||||
// ===== 수주 정보 (사이드바 선택 항목 기반) =====
|
||||
const orderInfo = useMemo(() => {
|
||||
// 1. 선택된 API 작업지시에서 찾기
|
||||
const apiOrder = filteredWorkOrders.find((wo) => wo.id === selectedSidebarOrderId);
|
||||
if (apiOrder) {
|
||||
return {
|
||||
orderDate: apiOrder.createdAt ? new Date(apiOrder.createdAt).toLocaleDateString('ko-KR') : '-',
|
||||
salesOrderNo: apiOrder.salesOrderNo || '-',
|
||||
siteName: apiOrder.projectName || '-',
|
||||
client: apiOrder.client || '-',
|
||||
salesManager: apiOrder.assignees?.[0] || '-',
|
||||
managerPhone: '-',
|
||||
shippingDate: apiOrder.dueDate ? new Date(apiOrder.dueDate).toLocaleDateString('ko-KR') : '-',
|
||||
};
|
||||
}
|
||||
|
||||
// 2. 목업 사이드바에서 찾기
|
||||
const mockOrder = MOCK_SIDEBAR_ORDERS[activeProcessTabKey].find((o) => o.id === selectedSidebarOrderId);
|
||||
if (mockOrder) {
|
||||
return {
|
||||
orderDate: mockOrder.date,
|
||||
salesOrderNo: 'SO-2024-0001',
|
||||
siteName: mockOrder.siteName,
|
||||
client: '-',
|
||||
salesManager: '-',
|
||||
managerPhone: '-',
|
||||
shippingDate: '-',
|
||||
};
|
||||
}
|
||||
|
||||
// 3. 폴백: 첫 번째 작업
|
||||
const first = filteredWorkOrders[0];
|
||||
if (!first) return null;
|
||||
return {
|
||||
orderDate: first.createdAt ? new Date(first.createdAt).toLocaleDateString('ko-KR') : '-',
|
||||
lotNo: '-',
|
||||
salesOrderNo: first.salesOrderNo || '-',
|
||||
siteName: first.projectName || '-',
|
||||
client: first.client || '-',
|
||||
salesManager: first.assignees?.[0] || '-',
|
||||
managerPhone: '-',
|
||||
shippingDate: first.dueDate ? new Date(first.dueDate).toLocaleDateString('ko-KR') : '-',
|
||||
};
|
||||
}, [filteredWorkOrders]);
|
||||
}, [filteredWorkOrders, selectedSidebarOrderId, activeProcessTabKey]);
|
||||
|
||||
// ===== 핸들러 =====
|
||||
|
||||
@@ -722,14 +828,14 @@ export default function WorkerScreen() {
|
||||
|
||||
// 현재 공정에 맞는 중간검사 타입 결정
|
||||
const getInspectionProcessType = useCallback((): InspectionProcessType => {
|
||||
if (activeTab === 'bending' && bendingSubMode === 'wip') {
|
||||
if (activeProcessTabKey === 'bending' && bendingSubMode === 'wip') {
|
||||
return 'bending_wip';
|
||||
}
|
||||
if (activeTab === 'slat' && slatSubMode === 'jointbar') {
|
||||
if (activeProcessTabKey === 'slat' && slatSubMode === 'jointbar') {
|
||||
return 'slat_jointbar';
|
||||
}
|
||||
return activeTab as InspectionProcessType;
|
||||
}, [activeTab, bendingSubMode, slatSubMode]);
|
||||
return activeProcessTabKey as InspectionProcessType;
|
||||
}, [activeProcessTabKey, bendingSubMode, slatSubMode]);
|
||||
|
||||
// 하단 버튼 핸들러
|
||||
const handleWorkLog = useCallback(() => {
|
||||
@@ -766,13 +872,13 @@ export default function WorkerScreen() {
|
||||
|
||||
// ===== 재공품 감지 =====
|
||||
const hasWipItems = useMemo(() => {
|
||||
return activeTab === 'bending' && workItems.some(item => item.isWip);
|
||||
}, [activeTab, workItems]);
|
||||
return activeProcessTabKey === 'bending' && workItems.some(item => item.isWip);
|
||||
}, [activeProcessTabKey, workItems]);
|
||||
|
||||
// ===== 조인트바 감지 =====
|
||||
const hasJointBarItems = useMemo(() => {
|
||||
return activeTab === 'slat' && slatSubMode === 'jointbar';
|
||||
}, [activeTab, slatSubMode]);
|
||||
return activeProcessTabKey === 'slat' && slatSubMode === 'jointbar';
|
||||
}, [activeProcessTabKey, slatSubMode]);
|
||||
|
||||
// 재공품 통합 문서 (작업일지 + 중간검사) 핸들러
|
||||
const handleWipInspection = useCallback(() => {
|
||||
@@ -802,21 +908,32 @@ export default function WorkerScreen() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 공정별 탭 */}
|
||||
{/* 공정별 탭 (공정관리 API 기반 동적 생성) */}
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onValueChange={(v) => setActiveTab(v as ProcessTab)}
|
||||
onValueChange={(v) => setActiveTab(v)}
|
||||
>
|
||||
<TabsList className="w-full">
|
||||
{(['screen', 'slat', 'bending'] as ProcessTab[]).map((tab) => (
|
||||
<TabsTrigger key={tab} value={tab} className="flex-1">
|
||||
{PROCESS_TAB_LABELS[tab]}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
{processTabs.length > 0 ? (
|
||||
processTabs.map((proc) => (
|
||||
<TabsTrigger key={proc.id} value={proc.id} className="flex-1">
|
||||
{proc.processName}
|
||||
</TabsTrigger>
|
||||
))
|
||||
) : (
|
||||
(['screen', 'slat', 'bending'] as ProcessTab[]).map((tab) => (
|
||||
<TabsTrigger key={tab} value={tab} className="flex-1">
|
||||
{PROCESS_TAB_LABELS[tab]}
|
||||
</TabsTrigger>
|
||||
))
|
||||
)}
|
||||
</TabsList>
|
||||
|
||||
{(['screen', 'slat', 'bending'] as ProcessTab[]).map((tab) => (
|
||||
<TabsContent key={tab} value={tab} className="mt-4">
|
||||
{(processTabs.length > 0
|
||||
? processTabs.map((p) => p.id)
|
||||
: ['screen', 'slat', 'bending']
|
||||
).map((tabValue) => (
|
||||
<TabsContent key={tabValue} value={tabValue} className="mt-4">
|
||||
{/* 모바일: 사이드바 토글 */}
|
||||
<div className="md:hidden mb-4">
|
||||
<button
|
||||
@@ -826,15 +943,16 @@ export default function WorkerScreen() {
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<List className="h-4 w-4" />
|
||||
<span className="text-sm font-medium">작업 목록</span>
|
||||
<span className="text-sm font-medium">수주 목록</span>
|
||||
</div>
|
||||
{isSidebarOpen ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
|
||||
</button>
|
||||
{isSidebarOpen && (
|
||||
<div className="mt-2 p-3 border rounded-lg bg-white max-h-[300px] overflow-y-auto">
|
||||
<SidebarContent
|
||||
tab={tab}
|
||||
tab={activeProcessTabKey}
|
||||
selectedOrderId={selectedSidebarOrderId}
|
||||
apiOrders={apiSidebarOrders}
|
||||
onSelectOrder={(id, subType) => {
|
||||
setSelectedSidebarOrderId(id);
|
||||
if (subType === 'slat' || subType === 'jointbar') setSlatSubMode(subType === 'jointbar' ? 'jointbar' : 'normal');
|
||||
@@ -853,8 +971,9 @@ export default function WorkerScreen() {
|
||||
<Card className="max-h-[calc(100vh-7.5rem)] overflow-y-auto">
|
||||
<CardContent className="p-4">
|
||||
<SidebarContent
|
||||
tab={tab}
|
||||
tab={activeProcessTabKey}
|
||||
selectedOrderId={selectedSidebarOrderId}
|
||||
apiOrders={apiSidebarOrders}
|
||||
onSelectOrder={(id, subType) => {
|
||||
setSelectedSidebarOrderId(id);
|
||||
if (subType === 'slat' || subType === 'jointbar') setSlatSubMode(subType === 'jointbar' ? 'jointbar' : 'normal');
|
||||
@@ -901,7 +1020,7 @@ export default function WorkerScreen() {
|
||||
<h3 className="text-sm font-semibold text-gray-900 mb-3">수주 정보</h3>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-x-6 gap-y-3">
|
||||
<InfoField label="수주일" value={orderInfo?.orderDate} />
|
||||
<InfoField label="로트번호" value={orderInfo?.lotNo} />
|
||||
<InfoField label="수주로트" value={orderInfo?.salesOrderNo} />
|
||||
<InfoField label="현장명" value={orderInfo?.siteName} />
|
||||
<InfoField label="수주처" value={orderInfo?.client} />
|
||||
<InfoField label="수주 담당자" value={orderInfo?.salesManager} />
|
||||
@@ -977,16 +1096,37 @@ export default function WorkerScreen() {
|
||||
</Card>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{workItems.map((item) => (
|
||||
<WorkItemCard
|
||||
key={item.id}
|
||||
item={item}
|
||||
onStepClick={handleStepClick}
|
||||
onEditMaterial={handleEditMaterial}
|
||||
onDeleteMaterial={handleDeleteMaterial}
|
||||
onInspectionClick={handleInspectionClick}
|
||||
/>
|
||||
))}
|
||||
{(() => {
|
||||
const apiCount = workItems.filter((i) => !i.id.startsWith('mock-')).length;
|
||||
return apiCount > 0 ? (
|
||||
<span className="text-[10px] font-semibold text-blue-600 bg-blue-50 px-2 py-0.5 rounded inline-block">
|
||||
실제 데이터 ({apiCount}건)
|
||||
</span>
|
||||
) : null;
|
||||
})()}
|
||||
{workItems.map((item, index) => {
|
||||
const isFirstMock = item.id.startsWith('mock-') &&
|
||||
(index === 0 || !workItems[index - 1]?.id.startsWith('mock-'));
|
||||
return (
|
||||
<div key={item.id}>
|
||||
{isFirstMock && (
|
||||
<div className="mb-3 pt-1 space-y-2">
|
||||
{workItems.some((i) => !i.id.startsWith('mock-')) && <div className="border-t border-dashed border-gray-300" />}
|
||||
<span className="text-[10px] font-semibold text-orange-600 bg-orange-50 px-2 py-0.5 rounded inline-block">
|
||||
목업 데이터
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<WorkItemCard
|
||||
item={item}
|
||||
onStepClick={handleStepClick}
|
||||
onEditMaterial={handleEditMaterial}
|
||||
onDeleteMaterial={handleDeleteMaterial}
|
||||
onInspectionClick={handleInspectionClick}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -1049,14 +1189,14 @@ export default function WorkerScreen() {
|
||||
open={isWorkLogModalOpen}
|
||||
onOpenChange={setIsWorkLogModalOpen}
|
||||
workOrderId={selectedOrder?.id || null}
|
||||
processType={activeTab}
|
||||
processType={activeProcessTabKey}
|
||||
/>
|
||||
|
||||
<InspectionReportModal
|
||||
open={isInspectionModalOpen}
|
||||
onOpenChange={setIsInspectionModalOpen}
|
||||
workOrderId={selectedOrder?.id || null}
|
||||
processType={hasWipItems ? 'bending_wip' : activeTab}
|
||||
processType={hasWipItems ? 'bending_wip' : activeProcessTabKey}
|
||||
readOnly={true}
|
||||
isJointBar={hasJointBarItems}
|
||||
inspectionData={selectedOrder ? inspectionDataMap.get(selectedOrder.id) : undefined}
|
||||
@@ -1094,6 +1234,7 @@ export default function WorkerScreen() {
|
||||
interface SidebarContentProps {
|
||||
tab: ProcessTab;
|
||||
selectedOrderId: string;
|
||||
apiOrders: SidebarOrder[];
|
||||
onSelectOrder: (id: string, subType?: SidebarOrder['subType']) => void;
|
||||
}
|
||||
|
||||
@@ -1101,14 +1242,12 @@ function SidebarContent({
|
||||
tab,
|
||||
selectedOrderId,
|
||||
onSelectOrder,
|
||||
apiOrders,
|
||||
}: SidebarContentProps) {
|
||||
const orders = MOCK_SIDEBAR_ORDERS[tab];
|
||||
const mockOrders = MOCK_SIDEBAR_ORDERS[tab];
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-sm font-semibold text-gray-900">작업 목록</h3>
|
||||
|
||||
{/* 우선순위별 작업지시 카드 + 태그 */}
|
||||
const renderOrders = (orders: SidebarOrder[]) => (
|
||||
<>
|
||||
{PRIORITY_GROUPS.map((group) => {
|
||||
const groupOrders = orders.filter((o) => o.priority === group.key);
|
||||
if (groupOrders.length === 0) return null;
|
||||
@@ -1140,7 +1279,7 @@ function SidebarContent({
|
||||
</div>
|
||||
<div className="flex items-center justify-between mt-1 text-gray-500">
|
||||
<span>{order.date}</span>
|
||||
<span>{order.quantity}/{order.shutterCount}소</span>
|
||||
<span>{order.shutterCount}개소</span>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
@@ -1149,6 +1288,37 @@ function SidebarContent({
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-sm font-semibold text-gray-900">수주 목록</h3>
|
||||
|
||||
{/* API 실제 데이터 */}
|
||||
{apiOrders.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<span className="text-[10px] font-semibold text-blue-600 bg-blue-50 px-2 py-0.5 rounded">
|
||||
실제 데이터 ({apiOrders.length}건)
|
||||
</span>
|
||||
{renderOrders(apiOrders)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 구분선 */}
|
||||
{apiOrders.length > 0 && mockOrders.length > 0 && (
|
||||
<div className="border-t border-dashed border-gray-300 my-1" />
|
||||
)}
|
||||
|
||||
{/* 목업 데이터 */}
|
||||
{mockOrders.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<span className="text-[10px] font-semibold text-orange-600 bg-orange-50 px-2 py-0.5 rounded">
|
||||
목업 데이터
|
||||
</span>
|
||||
{renderOrders(mockOrders)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
194
src/components/production/WorkerScreen/mockData.ts
Normal file
194
src/components/production/WorkerScreen/mockData.ts
Normal file
@@ -0,0 +1,194 @@
|
||||
/**
|
||||
* 작업자 화면 목업 데이터
|
||||
*
|
||||
* 실제 API 연동 전환 시 참조용으로 보존
|
||||
* USE_MOCK = true 일 때 API 데이터 뒤에 병합됨
|
||||
*/
|
||||
|
||||
import type { ProcessTab, WorkItemData } from './types';
|
||||
|
||||
// ===== 목업 ON/OFF 플래그 =====
|
||||
// true: API 데이터 + 목업 데이터 병합 (개발 중)
|
||||
// false: API 데이터만 사용 (운영)
|
||||
export const USE_MOCK = false;
|
||||
|
||||
// ===== 공정별 목업 데이터 =====
|
||||
export const MOCK_ITEMS: Record<ProcessTab, WorkItemData[]> = {
|
||||
screen: [
|
||||
{
|
||||
id: 'mock-s1', itemNo: 1, itemCode: 'KWWS03', itemName: '와이어', floor: '1층', code: 'FSS-01',
|
||||
width: 8260, height: 8350, quantity: 2, processType: 'screen',
|
||||
cuttingInfo: { width: 1210, sheets: 8 },
|
||||
steps: [
|
||||
{ id: 's1-1', name: '자재투입', isMaterialInput: true, isCompleted: true },
|
||||
{ id: 's1-2', name: '절단', isMaterialInput: false, isCompleted: true },
|
||||
{ id: 's1-3', name: '미싱', isMaterialInput: false, isCompleted: false },
|
||||
{ id: 's1-4', name: '포장완료', isMaterialInput: false, isCompleted: false },
|
||||
],
|
||||
materialInputs: [
|
||||
{ id: 'm1', lotNo: 'LOT-2026-001', itemName: '스크린 원단 A', quantity: 500, unit: 'm' },
|
||||
{ id: 'm2', lotNo: 'LOT-2026-002', itemName: '와이어 B', quantity: 120, unit: 'EA' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'mock-s2', itemNo: 2, itemCode: 'KWWS05', itemName: '메쉬', floor: '2층', code: 'FSS-03',
|
||||
width: 6400, height: 5200, quantity: 4, processType: 'screen',
|
||||
cuttingInfo: { width: 1600, sheets: 4 },
|
||||
steps: [
|
||||
{ id: 's2-1', name: '자재투입', isMaterialInput: true, isCompleted: false },
|
||||
{ id: 's2-2', name: '절단', isMaterialInput: false, isCompleted: false },
|
||||
{ id: 's2-3', name: '미싱', isMaterialInput: false, isCompleted: false },
|
||||
{ id: 's2-4', name: '포장완료', isMaterialInput: false, isCompleted: false },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'mock-s3', itemNo: 3, itemCode: 'KWWS08', itemName: '와이어(광폭)', floor: '3층', code: 'FSS-05',
|
||||
width: 12000, height: 4500, quantity: 1, processType: 'screen',
|
||||
cuttingInfo: { width: 2400, sheets: 5 },
|
||||
steps: [
|
||||
{ id: 's3-1', name: '자재투입', isMaterialInput: true, isCompleted: true },
|
||||
{ id: 's3-2', name: '절단', isMaterialInput: false, isCompleted: true },
|
||||
{ id: 's3-3', name: '미싱', isMaterialInput: false, isCompleted: true },
|
||||
{ id: 's3-4', name: '포장완료', isMaterialInput: false, isCompleted: false },
|
||||
],
|
||||
materialInputs: [
|
||||
{ id: 'm3', lotNo: 'LOT-2026-005', itemName: '광폭 원단', quantity: 300, unit: 'm' },
|
||||
],
|
||||
},
|
||||
],
|
||||
slat: [
|
||||
{
|
||||
id: 'mock-l1', itemNo: 1, itemCode: 'KQTS01', itemName: '슬랫코일', floor: '1층', code: 'FSS-01',
|
||||
width: 8260, height: 8350, quantity: 2, processType: 'slat',
|
||||
slatInfo: { length: 3910, slatCount: 40, jointBar: 4 },
|
||||
steps: [
|
||||
{ id: 'l1-1', name: '자재투입', isMaterialInput: true, isCompleted: true },
|
||||
{ id: 'l1-2', name: '포밍/절단', isMaterialInput: false, isCompleted: false },
|
||||
{ id: 'l1-3', name: '포장완료', isMaterialInput: false, isCompleted: false },
|
||||
],
|
||||
materialInputs: [
|
||||
{ id: 'm4', lotNo: 'LOT-2026-010', itemName: '슬랫 코일 A', quantity: 200, unit: 'kg' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'mock-l2', itemNo: 2, itemCode: 'KQTS03', itemName: '슬랫코일(광폭)', floor: '2층', code: 'FSS-02',
|
||||
width: 10500, height: 6200, quantity: 3, processType: 'slat',
|
||||
slatInfo: { length: 5200, slatCount: 55, jointBar: 6 },
|
||||
steps: [
|
||||
{ id: 'l2-1', name: '자재투입', isMaterialInput: true, isCompleted: false },
|
||||
{ id: 'l2-2', name: '포밍/절단', isMaterialInput: false, isCompleted: false },
|
||||
{ id: 'l2-3', name: '포장완료', isMaterialInput: false, isCompleted: false },
|
||||
],
|
||||
},
|
||||
],
|
||||
bending: [
|
||||
{
|
||||
id: 'mock-b1', itemNo: 1, itemCode: 'KWWS03', itemName: '가이드레일', floor: '1층', code: 'FSS-01',
|
||||
width: 0, height: 0, quantity: 6, processType: 'bending',
|
||||
bendingInfo: {
|
||||
common: {
|
||||
kind: '혼합형 120X70', type: '혼합형',
|
||||
lengthQuantities: [{ length: 4000, quantity: 6 }, { length: 3000, quantity: 6 }],
|
||||
},
|
||||
detailParts: [
|
||||
{ partName: '엘바', material: 'EGI 1.6T', barcyInfo: '16 I 75' },
|
||||
{ partName: '하장바', material: 'EGI 1.6T', barcyInfo: '16|75|16|75|16(A각)' },
|
||||
],
|
||||
},
|
||||
steps: [
|
||||
{ id: 'b1-1', name: '자재투입', isMaterialInput: true, isCompleted: true },
|
||||
{ id: 'b1-2', name: '절단', isMaterialInput: false, isCompleted: true },
|
||||
{ id: 'b1-3', name: '절곡', isMaterialInput: false, isCompleted: false },
|
||||
{ id: 'b1-4', name: '포장완료', isMaterialInput: false, isCompleted: false },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// ===== 절곡 재공품 전용 목업 데이터 =====
|
||||
export const MOCK_ITEMS_BENDING_WIP: WorkItemData[] = [
|
||||
{
|
||||
id: 'mock-bw1', itemNo: 1, itemCode: 'KWWS03', itemName: '케이스 - 전면부', floor: '-', code: '-',
|
||||
width: 0, height: 0, quantity: 6, processType: 'bending',
|
||||
isWip: true,
|
||||
wipInfo: { specification: 'EGI 1.55T (W576)', lengthQuantity: '4,000mm X 6개' },
|
||||
steps: [
|
||||
{ id: 'bw1-1', name: '자재투입', isMaterialInput: true, isCompleted: false },
|
||||
{ id: 'bw1-2', name: '절단', isMaterialInput: false, isCompleted: false },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'mock-bw2', itemNo: 2, itemCode: 'KWWS04', itemName: '케이스 - 후면부', floor: '-', code: '-',
|
||||
width: 0, height: 0, quantity: 4, processType: 'bending',
|
||||
isWip: true,
|
||||
wipInfo: { specification: 'EGI 1.55T (W576)', lengthQuantity: '3,500mm X 4개' },
|
||||
steps: [
|
||||
{ id: 'bw2-1', name: '자재투입', isMaterialInput: true, isCompleted: false },
|
||||
{ id: 'bw2-2', name: '절단', isMaterialInput: false, isCompleted: false },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'mock-bw3', itemNo: 3, itemCode: 'KWWS05', itemName: '하단마감재', floor: '-', code: '-',
|
||||
width: 0, height: 0, quantity: 10, processType: 'bending',
|
||||
isWip: true,
|
||||
wipInfo: { specification: 'EGI 1.2T (W400)', lengthQuantity: '2,800mm X 10개' },
|
||||
steps: [
|
||||
{ id: 'bw3-1', name: '자재투입', isMaterialInput: true, isCompleted: false },
|
||||
{ id: 'bw3-2', name: '절단', isMaterialInput: false, isCompleted: false },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// ===== 슬랫 조인트바 전용 목업 데이터 =====
|
||||
export const MOCK_ITEMS_SLAT_JOINTBAR: WorkItemData[] = [
|
||||
{
|
||||
id: 'mock-jb1', itemNo: 1, itemCode: 'KQJB01', itemName: '조인트바 A', floor: '1층', code: 'FSS-01',
|
||||
width: 0, height: 0, quantity: 8, processType: 'slat',
|
||||
isJointBar: true,
|
||||
slatJointBarInfo: { specification: 'EGI 1.6T', length: 3910, quantity: 8 },
|
||||
steps: [
|
||||
{ id: 'jb1-1', name: '자재투입', isMaterialInput: true, isCompleted: true },
|
||||
{ id: 'jb1-2', name: '포밍/절단', isMaterialInput: false, isCompleted: false },
|
||||
{ id: 'jb1-3', name: '포장완료', isMaterialInput: false, isCompleted: false },
|
||||
],
|
||||
materialInputs: [
|
||||
{ id: 'mjb1', lotNo: 'LOT-2026-020', itemName: '조인트바 코일', quantity: 100, unit: 'kg' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'mock-jb2', itemNo: 2, itemCode: 'KQJB02', itemName: '조인트바 B', floor: '2층', code: 'FSS-02',
|
||||
width: 0, height: 0, quantity: 12, processType: 'slat',
|
||||
isJointBar: true,
|
||||
slatJointBarInfo: { specification: 'EGI 1.6T', length: 5200, quantity: 12 },
|
||||
steps: [
|
||||
{ id: 'jb2-1', name: '자재투입', isMaterialInput: true, isCompleted: false },
|
||||
{ id: 'jb2-2', name: '포밍/절단', isMaterialInput: false, isCompleted: false },
|
||||
{ id: 'jb2-3', name: '포장완료', isMaterialInput: false, isCompleted: false },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// ===== 하드코딩된 공정별 단계 폴백 =====
|
||||
export const PROCESS_STEPS_FALLBACK: Record<string, { name: string; isMaterialInput: boolean }[]> = {
|
||||
screen: [
|
||||
{ name: '자재투입', isMaterialInput: true },
|
||||
{ name: '절단', isMaterialInput: false },
|
||||
{ name: '미싱', isMaterialInput: false },
|
||||
{ name: '포장완료', isMaterialInput: false },
|
||||
],
|
||||
slat: [
|
||||
{ name: '자재투입', isMaterialInput: true },
|
||||
{ name: '포밍/절단', isMaterialInput: false },
|
||||
{ name: '포장완료', isMaterialInput: false },
|
||||
],
|
||||
bending: [
|
||||
{ name: '자재투입', isMaterialInput: true },
|
||||
{ name: '절단', isMaterialInput: false },
|
||||
{ name: '절곡', isMaterialInput: false },
|
||||
{ name: '포장완료', isMaterialInput: false },
|
||||
],
|
||||
bending_wip: [
|
||||
{ name: '자재투입', isMaterialInput: true },
|
||||
{ name: '절단', isMaterialInput: false },
|
||||
],
|
||||
};
|
||||
Reference in New Issue
Block a user