feat: [작업자화면] 공정 그룹(process_group) 도입 — 절곡 탭 1개로 통합 + 하위 공정 필터

This commit is contained in:
김보곤
2026-03-21 15:07:37 +09:00
parent 728c9c7a29
commit 1c86f5c8f6
3 changed files with 106 additions and 15 deletions

View File

@@ -103,6 +103,7 @@ function transformApiToFrontend(apiData: ApiProcess): Process {
workLogTemplateName: apiData.work_log_template_relation?.name ?? undefined,
needsInspection: apiData.options?.needs_inspection ?? false,
needsWorkLog: apiData.options?.needs_work_log ?? false,
processGroup: apiData.options?.process_group ?? undefined,
classificationRules: [...patternRules, ...individualRules],
requiredWorkers: apiData.required_workers,
equipmentInfo: apiData.equipment_info ?? undefined,

View File

@@ -284,20 +284,50 @@ export default function WorkerScreen() {
// 공정 목록 캐시
const [processListCache, setProcessListCache] = useState<Process[]>([]);
// 활성 공정 목록 (탭용) - 공정관리에서 등록된 활성 공정만
const processTabs = useMemo(() => {
// 활성 공정 목록
const activeProcesses = useMemo(() => {
return processListCache.filter((p) => p.status === '사용중');
}, [processListCache]);
// 공정 목록 로드 후 첫 번째 공정을 기본 선택 (폴백: 'screen')
// 그룹별 탭 구성 (process_group 기준, 없으면 공정명 사용)
const groupedTabs = useMemo(() => {
const groupMap = new Map<string, Process[]>();
activeProcesses.forEach((p) => {
const group = p.processGroup || p.processName;
if (!groupMap.has(group)) groupMap.set(group, []);
groupMap.get(group)!.push(p);
});
return Array.from(groupMap.entries()).map(([group, processes]) => ({
group,
processes,
// 그룹 내 첫 번째 공정 ID를 탭 value로 사용
defaultProcessId: processes[0].id,
}));
}, [activeProcesses]);
// processTabs 호환 (기존 코드에서 참조)
const processTabs = activeProcesses;
// 선택된 그룹 내 하위 공정 필터
const [subProcessId, setSubProcessId] = useState<string>('all');
// 현재 탭의 그룹 정보
const activeGroup = useMemo(() => {
const process = processListCache.find((p) => p.id === activeTab);
if (!process) return null;
const group = process.processGroup || process.processName;
return groupedTabs.find((g) => g.group === group) || null;
}, [activeTab, processListCache, groupedTabs]);
// 공정 목록 로드 후 첫 번째 그룹을 기본 선택
useEffect(() => {
if (activeTab) return;
if (processTabs.length > 0) {
setActiveTab(processTabs[0].id);
if (groupedTabs.length > 0) {
setActiveTab(groupedTabs[0].defaultProcessId);
} else if (!isLoading) {
setActiveTab('screen');
}
}, [processTabs, activeTab, isLoading]);
}, [groupedTabs, activeTab, isLoading]);
// 선택된 공정의 ProcessTab 키 (mock 데이터 및 기존 로직 호환용)
const activeProcessTabKey: ProcessTab = useMemo(() => {
@@ -358,17 +388,40 @@ export default function WorkerScreen() {
}, [selectedSidebarOrderId]);
// ===== 탭별 필터링된 작업 =====
// ===== 탭별 필터링된 작업 (그룹 + 하위 공정 필터) =====
const filteredWorkOrders = useMemo(() => {
// 하위 공정이 특정되면 해당 공정만 필터
if (subProcessId !== 'all') {
const subProcess = processListCache.find((p) => p.id === subProcessId);
if (subProcess) {
const subName = subProcess.processName.toLowerCase();
return workOrders.filter((order) => {
const orderProcessName = (order.processName || '').toLowerCase();
return orderProcessName.includes(subName) || subName.includes(orderProcessName);
});
}
}
// 그룹 내 모든 공정 매칭
if (activeGroup) {
const groupProcessNames = activeGroup.processes.map((p) => p.processName.toLowerCase());
return workOrders.filter((order) => {
const orderProcessName = (order.processName || '').toLowerCase();
return groupProcessNames.some((gpn) =>
orderProcessName.includes(gpn) || gpn.includes(orderProcessName)
);
});
}
// 폴백: 기존 방식
const selectedProcess = processListCache.find((p) => p.id === activeTab);
if (!selectedProcess) return workOrders;
const selectedName = selectedProcess.processName.toLowerCase();
return workOrders.filter((order) => {
const orderProcessName = (order.processName || '').toLowerCase();
return orderProcessName.includes(selectedName) || selectedName.includes(orderProcessName);
});
}, [workOrders, activeTab, processListCache]);
}, [workOrders, activeTab, activeGroup, subProcessId, processListCache]);
// ===== API WorkOrders → SidebarOrder 변환 =====
const apiSidebarOrders: SidebarOrder[] = useMemo(() => {
@@ -1313,10 +1366,15 @@ export default function WorkerScreen() {
>
<div className="overflow-x-auto -mx-3 px-3 md:mx-0 md:px-0">
<TabsList className="w-max md:w-full">
{processTabs.length > 0 ? (
processTabs.map((proc) => (
<TabsTrigger key={proc.id} value={proc.id} className="px-4 md:flex-1">
{proc.processName}
{groupedTabs.length > 0 ? (
groupedTabs.map((g) => (
<TabsTrigger
key={g.defaultProcessId}
value={g.defaultProcessId}
className="px-4 md:flex-1"
onClick={() => { setSubProcessId('all'); }}
>
{g.group}
</TabsTrigger>
))
) : (
@@ -1329,8 +1387,37 @@ export default function WorkerScreen() {
</TabsList>
</div>
{(processTabs.length > 0
? processTabs.map((p) => p.id)
{/* 그룹 내 하위 공정 필터 (2개 이상일 때만 표시) */}
{activeGroup && activeGroup.processes.length > 1 && (
<div className="flex items-center gap-2 mt-2 px-1 overflow-x-auto">
<button
type="button"
onClick={() => setSubProcessId('all')}
className={cn(
'px-3 py-1 rounded-full text-xs font-medium whitespace-nowrap transition-colors',
subProcessId === 'all' ? 'bg-blue-600 text-white' : 'bg-gray-100 text-gray-600 hover:bg-gray-200'
)}
>
</button>
{activeGroup.processes.map((p) => (
<button
key={p.id}
type="button"
onClick={() => setSubProcessId(p.id)}
className={cn(
'px-3 py-1 rounded-full text-xs font-medium whitespace-nowrap transition-colors',
subProcessId === p.id ? 'bg-blue-600 text-white' : 'bg-gray-100 text-gray-600 hover:bg-gray-200'
)}
>
{p.processName.replace(activeGroup.group, '').replace(/[()]/g, '').trim() || p.processName}
</button>
))}
</div>
)}
{(groupedTabs.length > 0
? groupedTabs.map((g) => g.defaultProcessId)
: ['screen', 'slat', 'bending']
).map((tabValue) => (
<TabsContent key={tabValue} value={tabValue} className="mt-4">

View File

@@ -92,6 +92,9 @@ export interface Process {
// 단계 목록
steps?: ProcessStep[];
// 공정 그룹 (같은 그룹은 하나의 탭으로 표시)
processGroup?: string;
// 상태
status: ProcessStatus;