feat: [작업자화면] 수주/재공품 탭 분리, BD 코드 필터링, 전개도 폭+길이 표시

This commit is contained in:
김보곤
2026-03-21 08:00:06 +09:00
parent b783e44618
commit bf49a59825
5 changed files with 146 additions and 18 deletions

View File

@@ -58,6 +58,7 @@ export interface WorkOrderNodeGroup {
// 개소 내 개별 아이템
export interface WorkOrderNodeItem {
id: number;
itemCode?: string | null;
itemName: string;
quantity: number;
specification?: string | null;

View File

@@ -76,9 +76,16 @@ export const WorkItemCard = memo(function WorkItemCard({
<span className="flex items-center justify-center w-8 h-8 rounded-full bg-emerald-500 text-white text-sm font-bold shrink-0">
{item.itemNo}
</span>
<span className="text-base font-bold text-gray-900">
{item.itemCode} - {item.itemName}
</span>
<div>
<span className="text-base font-bold text-gray-900">
{item.itemCode} - {item.itemName}
</span>
{item.bdCode && (
<span className="ml-2 text-xs font-mono text-blue-600 bg-blue-50 px-1.5 py-0.5 rounded">
{item.bdCode}
</span>
)}
</div>
</div>
{!item.isWip && (
<span className="text-sm text-gray-500 shrink-0">

View File

@@ -157,6 +157,7 @@ function transformToWorkerScreenFormat(api: WorkOrderApiItem): WorkOrder {
nodeName: g.nodeName,
items: (g.items || []).map((it) => ({
id: it.id,
itemCode: it.item?.code || null,
itemName: it.item_name,
quantity: Number(it.quantity),
specification: it.specification,

View File

@@ -13,6 +13,13 @@
* - 하단 고정 버튼 (작업일지보기 / 중간검사하기)
*/
/** 품목명에서 길이(mm) 추출: "가이드레일(측면) 본체(철재) 2438mm" → 2438 */
function extractLengthFromName(name?: string | null): number {
if (!name) return 0;
const m = name.match(/(\d{3,5})\s*mm/i);
return m ? parseInt(m[1], 10) : 0;
}
import { useState, useMemo, useCallback, useEffect } from 'react';
import dynamic from 'next/dynamic';
import { useSidebarCollapsed } from '@/stores/menuStore';
@@ -47,6 +54,7 @@ import type { InspectionTemplateData } from './types';
import { getProcessList } from '@/components/process-management/actions';
import type { InspectionSetting, InspectionScope, Process } from '@/types/process';
import type { WorkOrder } from '../ProductionDashboard/types';
import { BENDING_STEP_MAP, extractBendingTypeCode } from '../WorkOrders/types';
import { isNextRedirectError } from '@/lib/utils/redirect-error';
import type {
WorkerStats,
@@ -90,6 +98,7 @@ interface SidebarOrder {
shutterCount: number;
priority: 'urgent' | 'priority' | 'normal';
subType?: 'slat' | 'jointbar' | 'bending' | 'wip';
bdCode?: string; // 재공품 BD- 코드 (예: BD-ST-24)
}
const SUB_TYPE_TAGS: Record<string, { label: string; className: string }> = {
@@ -363,14 +372,23 @@ export default function WorkerScreen() {
// ===== 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'],
}));
return filteredWorkOrders.map((wo) => {
const isWip = wo.projectName === '재고생산' || wo.salesOrderNo?.startsWith('STK');
// 재공품: 첫 번째 item의 BD- 코드 추출
const bdCode = isWip
? wo.nodeGroups?.flatMap(g => g.items).find(it => it.itemCode?.startsWith('BD-'))?.itemCode ?? undefined
: undefined;
return {
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'],
subType: isWip ? 'wip' as const : undefined,
bdCode,
};
});
}, [filteredWorkOrders]);
// ===== 탭 변경/데이터 로드 시 최상위 우선순위 작업 자동 선택 =====
@@ -480,7 +498,42 @@ export default function WorkerScreen() {
const nodeKey = group.nodeId != null ? String(group.nodeId) : `unassigned-${index}`;
const firstItem = group.items[0];
const firstItemId = firstItem?.id as number | undefined;
const steps: WorkStepData[] = stepsTemplate.map((st, si) => {
// 절곡 공정: BD 코드에 따라 필요한 단계만 필터링
let itemStepsTemplate = stepsTemplate;
if (activeProcessTabKey === 'bending') {
const itemCodes = group.items.map((it) => it.itemCode).filter(Boolean) as string[];
if (itemCodes.length > 0) {
const neededKeys = new Set<string>();
let hasNonBd = false;
for (const code of itemCodes) {
const typeCode = extractBendingTypeCode(code);
if (typeCode && BENDING_STEP_MAP[typeCode]) {
BENDING_STEP_MAP[typeCode].forEach((k) => neededKeys.add(k));
} else {
hasNonBd = true;
}
}
if (!hasNonBd && neededKeys.size > 0) {
// 단계명 매핑: guide_rail→가이드레일 제작, case→케이스 제작, bottom_finish→하단마감재 제작, inspection→검사/중간검사
const stepNameMap: Record<string, string[]> = {
guide_rail: ['가이드레일 제작', '가이드레일'],
case: ['케이스 제작', '케이스'],
bottom_finish: ['하단마감재 제작', '하단마감재'],
inspection: ['검사', '중간검사'],
};
const allowedNames = new Set<string>();
// 자재투입은 항상 포함
allowedNames.add('자재투입');
for (const key of neededKeys) {
(stepNameMap[key] || []).forEach((n) => allowedNames.add(n));
}
itemStepsTemplate = stepsTemplate.filter((st) => allowedNames.has(st.name));
}
}
}
const steps: WorkStepData[] = itemStepsTemplate.map((st, si) => {
const stepKey = `${selectedOrder.id}-${nodeKey}-${st.name}`;
return enrichStep(st, `${selectedOrder.id}-${nodeKey}-step-${si}`, stepKey, firstItemId);
});
@@ -529,8 +582,8 @@ export default function WorkerScreen() {
itemName: selectedOrder.productCode !== '-' ? selectedOrder.productCode : itemSummary,
floor: (opts.floor as string) || '-',
code: (opts.code as string) || '-',
width: (opts.width as number) || 0,
height: (opts.height as number) || 0,
width: (opts.bending_width as number) || (opts.width as number) || 0,
height: (opts.height as number) || extractLengthFromName(firstItem?.itemName) || 0,
quantity: group.totalQuantity,
processType: activeProcessTabKey,
steps,
@@ -556,6 +609,11 @@ export default function WorkerScreen() {
detailParts: (bi.detail_parts || []).map(dp => ({ partName: dp.part_name, material: dp.material, barcyInfo: dp.barcy_info })),
};
}
// BD- 코드 추출 (재공품/절곡품 참조용)
const bdItemCode = firstItem?.itemCode;
if (bdItemCode?.startsWith('BD-')) {
workItem.bdCode = bdItemCode;
}
if (opts.is_wip) {
workItem.isWip = true;
const wi = opts.wip_info as { specification: string; length_quantity: string } | undefined;
@@ -1617,6 +1675,27 @@ function SidebarContent({
onSelectOrder,
apiOrders,
}: SidebarContentProps) {
const [sidebarTab, setSidebarTab] = useState<'orders' | 'wip'>('orders');
const [searchTerm, setSearchTerm] = useState('');
// 수주목록 / 재공품 분리
const regularOrders = useMemo(() =>
apiOrders.filter((o) => o.subType !== 'wip'),
[apiOrders]);
const wipOrders = useMemo(() =>
apiOrders.filter((o) => o.subType === 'wip'),
[apiOrders]);
// 검색 필터링
const displayOrders = useMemo(() => {
const source = sidebarTab === 'orders' ? regularOrders : wipOrders;
if (!searchTerm.trim()) return source;
const q = searchTerm.toLowerCase();
return source.filter((o) =>
o.siteName.toLowerCase().includes(q) || o.date.includes(q)
);
}, [sidebarTab, regularOrders, wipOrders, searchTerm]);
const renderOrders = (orders: SidebarOrder[]) => (
<>
{PRIORITY_GROUPS.map((group) => {
@@ -1648,6 +1727,9 @@ function SidebarContent({
</span>
)}
</div>
{order.bdCode && (
<p className="text-[10px] text-blue-600 font-mono mt-0.5">{order.bdCode}</p>
)}
<div className="flex items-center justify-between mt-1 text-gray-500">
<span>{order.date}</span>
<span>{order.shutterCount}</span>
@@ -1664,12 +1746,48 @@ function SidebarContent({
return (
<div className="space-y-3">
<h3 className="text-sm font-semibold text-gray-900"> </h3>
{/* 탭: 수주목록 / 재공품 */}
<div className="flex border-b">
<button
type="button"
onClick={() => { setSidebarTab('orders'); setSearchTerm(''); }}
className={cn(
'flex-1 text-sm font-medium py-2 border-b-2 transition-colors',
sidebarTab === 'orders'
? 'border-primary text-primary'
: 'border-transparent text-gray-500 hover:text-gray-700'
)}
>
({regularOrders.length})
</button>
<button
type="button"
onClick={() => { setSidebarTab('wip'); setSearchTerm(''); }}
className={cn(
'flex-1 text-sm font-medium py-2 border-b-2 transition-colors',
sidebarTab === 'wip'
? 'border-primary text-primary'
: 'border-transparent text-gray-500 hover:text-gray-700'
)}
>
({wipOrders.length})
</button>
</div>
{apiOrders.length > 0 ? (
renderOrders(apiOrders)
{/* 검색 */}
<Input
placeholder="현장명 검색..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="h-8 text-xs"
/>
{displayOrders.length > 0 ? (
renderOrders(displayOrders)
) : (
<p className="text-xs text-gray-400 text-center py-4"> .</p>
<p className="text-xs text-gray-400 text-center py-4">
{searchTerm ? '검색 결과가 없습니다.' : '데이터가 없습니다.'}
</p>
)}
</div>
);

View File

@@ -45,6 +45,7 @@ export interface WorkItemData {
processType: ProcessTab; // 공정 타입
steps: WorkStepData[]; // 공정 단계들
isWip?: boolean; // 재공품 여부
bdCode?: string; // 절곡품 BD- 코드 (예: BD-ST-24)
isJointBar?: boolean; // 조인트바 여부
// 스크린 전용
cuttingInfo?: CuttingInfo;