fix: [inspection] 절곡 검사성적서 재공품 대응 통합 수정

- 검사부위 공백 수정 (템플릿 컬럼 "부위" 라벨 매칭)
- hasWipItems 판정 보완 (sidebar order fallback)
- bending_wip 7제품 폼 통합 (products 배열 저장)
- 도면치수 실제 품목 길이 반영 (3000 하드코딩 제거)
- 테스트입력 버튼 7제품 데이터 채우기
- 하단 버튼 분리 유지 (작업일지/검사성적서)
- STOCK 단일부품 해당 부품만 검사항목 표시
- bendingInfo 기반 동적 검사 제품 생성
- 작업일지 LOT NO 원자재 투입 로트번호 표시
This commit is contained in:
김보곤
2026-03-21 21:21:06 +09:00
parent d91057aeb1
commit f483cff206
10 changed files with 441 additions and 238 deletions

View File

@@ -16,6 +16,7 @@
import { useState, useCallback, useMemo, forwardRef, useImperativeHandle, useEffect } from 'react';
import type { WorkOrder } from '../types';
import type { BendingInfoExtended } from './bending/types';
import type { InspectionData } from '@/components/production/WorkerScreen/InspectionInputModal';
import type { WorkItemData } from '@/components/production/WorkerScreen/types';
import type { InspectionDataMap } from './InspectionReportModal';
@@ -68,71 +69,140 @@ interface ProductRow {
gapPoints: GapPoint[];
}
const INITIAL_PRODUCTS: Omit<ProductRow, 'bendingStatus' | 'lengthMeasured' | 'widthMeasured'>[] = [
{
id: 'guide-rail-wall', category: 'KWE01', productName: '가이드레일', productType: '벽면형',
lengthDesign: '3000', widthDesign: 'N/A',
gapPoints: [
{ point: '', designValue: '30', measured: '' },
{ point: '②', designValue: '80', measured: '' },
{ point: '③', designValue: '45', measured: '' },
{ point: '', designValue: '40', measured: '' },
{ point: '', designValue: '34', measured: '' },
],
},
{
id: 'guide-rail-side', category: 'KWE01', productName: '가이드레일', productType: '측면형',
lengthDesign: '3000', widthDesign: 'N/A',
gapPoints: [
{ point: '①', designValue: '28', measured: '' },
{ point: '', designValue: '75', measured: '' },
{ point: '③', designValue: '42', measured: '' },
{ point: '', designValue: '38', measured: '' },
{ point: '', designValue: '32', measured: '' },
],
},
{
id: 'case', category: 'KWE01', productName: '케이스', productType: '500X380',
lengthDesign: '3000', widthDesign: 'N/A',
gapPoints: [
{ point: '①', designValue: '380', measured: '' },
{ point: '②', designValue: '50', measured: '' },
{ point: '③', designValue: '240', measured: '' },
{ point: '④', designValue: '50', measured: '' },
],
},
{
id: 'bottom-finish', category: 'KWE01', productName: '하단마감재', productType: '60X40',
lengthDesign: '3000', widthDesign: 'N/A',
gapPoints: [
{ point: '②', designValue: '60', measured: '' },
{ point: '②', designValue: '64', measured: '' },
],
},
{
id: 'bottom-l-bar', category: 'KWE01', productName: '하단L-BAR', productType: '17X60',
lengthDesign: '3000', widthDesign: 'N/A',
gapPoints: [
{ point: '①', designValue: '17', measured: '' },
],
},
{
id: 'smoke-w50', category: 'KWE01', productName: '연기차단재', productType: 'W50\n가이드레일용',
lengthDesign: '3000', widthDesign: '',
gapPoints: [
{ point: '①', designValue: '50', measured: '' },
{ point: '②', designValue: '12', measured: '' },
],
},
{
id: 'smoke-w80', category: 'KWE01', productName: '연기차단재', productType: 'W80\n케이스용',
lengthDesign: '3000', widthDesign: '',
gapPoints: [
{ point: '①', designValue: '80', measured: '' },
{ point: '②', designValue: '12', measured: '' },
],
},
];
/** 기본 gap 프로파일 (bendingInfo 없을 때 폴백) */
const DEFAULT_GAP_PROFILES = {
wall: [
{ point: '①', designValue: '30' }, { point: '②', designValue: '80' },
{ point: '③', designValue: '45' }, { point: '④', designValue: '40' },
{ point: '', designValue: '34' },
],
side: [
{ point: '', designValue: '28' }, { point: '②', designValue: '75' },
{ point: '', designValue: '42' }, { point: '④', designValue: '38' },
{ point: '⑤', designValue: '32' },
],
case: [
{ point: '①', designValue: '380' }, { point: '', designValue: '50' },
{ point: '③', designValue: '240' }, { point: '④', designValue: '50' },
],
bottomBar: [
{ point: '', designValue: '60' }, { point: '②', designValue: '64' },
],
lBar: [{ point: '', designValue: '17' }],
smokeW50: [{ point: '', designValue: '50' }, { point: '②', designValue: '12' }],
smokeW80: [{ point: '①', designValue: '80' }, { point: '②', designValue: '12' }],
};
type ProductTemplate = Omit<ProductRow, 'bendingStatus' | 'lengthMeasured' | 'widthMeasured'>;
/** bendingInfo 기반으로 검사 대상 부품 동적 생성 */
function buildProductsFromBendingInfo(bendingInfo?: BendingInfoExtended): ProductTemplate[] {
if (!bendingInfo?.productCode) return [];
const code = bendingInfo.productCode;
const isStock = bendingInfo.isStockProduction;
const stockKeys = new Set(bendingInfo.stockPartFilter?.map(f => f.partKey) || []);
// 길이 추출 헬퍼
const getLength = (data?: { lengthData?: Array<{ length: number }> }): string => {
const len = data?.lengthData?.[0]?.length;
return len ? String(len) : '3000';
};
const products: ProductTemplate[] = [];
// 가이드레일 벽면
const hasWall = bendingInfo?.guideRail?.wall;
if (hasWall && (!isStock || stockKeys.size === 0)) {
products.push({
id: 'guide-rail-wall', category: code, productName: '가이드레일', productType: '벽면형',
lengthDesign: getLength(hasWall), widthDesign: 'N/A',
gapPoints: DEFAULT_GAP_PROFILES.wall.map(p => ({ ...p, measured: '' })),
});
}
// 가이드레일 측면
const hasSide = bendingInfo?.guideRail?.side;
if (hasSide) {
// STOCK 필터: 가이드레일 관련 부품이면 표시
if (!isStock || stockKeys.size === 0 || ['본체', 'C형', 'D형', '마감재', '별도마감'].some(k => stockKeys.has(k))) {
products.push({
id: 'guide-rail-side', category: code, productName: '가이드레일', productType: '측면형',
lengthDesign: getLength(hasSide), widthDesign: 'N/A',
gapPoints: DEFAULT_GAP_PROFILES.side.map(p => ({ ...p, measured: '' })),
});
}
}
// 케이스
if (bendingInfo?.shutterBox && bendingInfo.shutterBox.length > 0) {
const box = bendingInfo.shutterBox[0];
products.push({
id: 'case', category: code, productName: '케이스', productType: box.size || '500X380',
lengthDesign: getLength(box), widthDesign: 'N/A',
gapPoints: DEFAULT_GAP_PROFILES.case.map(p => ({ ...p, measured: '' })),
});
} else if (!isStock) {
products.push({
id: 'case', category: code, productName: '케이스', productType: '500X380',
lengthDesign: '3000', widthDesign: 'N/A',
gapPoints: DEFAULT_GAP_PROFILES.case.map(p => ({ ...p, measured: '' })),
});
}
// 하단마감재
if (bendingInfo?.bottomBar) {
const len = bendingInfo.bottomBar.length3000Qty > 0 ? '3000' : bendingInfo.bottomBar.length4000Qty > 0 ? '4000' : '3000';
products.push({
id: 'bottom-finish', category: code, productName: '하단마감재', productType: '60X40',
lengthDesign: len, widthDesign: 'N/A',
gapPoints: DEFAULT_GAP_PROFILES.bottomBar.map(p => ({ ...p, measured: '' })),
});
products.push({
id: 'bottom-l-bar', category: code, productName: '하단L-BAR', productType: '17X60',
lengthDesign: len, widthDesign: 'N/A',
gapPoints: DEFAULT_GAP_PROFILES.lBar.map(p => ({ ...p, measured: '' })),
});
} else if (!isStock) {
products.push({
id: 'bottom-finish', category: code, productName: '하단마감재', productType: '60X40',
lengthDesign: '3000', widthDesign: 'N/A',
gapPoints: DEFAULT_GAP_PROFILES.bottomBar.map(p => ({ ...p, measured: '' })),
});
products.push({
id: 'bottom-l-bar', category: code, productName: '하단L-BAR', productType: '17X60',
lengthDesign: '3000', widthDesign: 'N/A',
gapPoints: DEFAULT_GAP_PROFILES.lBar.map(p => ({ ...p, measured: '' })),
});
}
// 연기차단재
if (bendingInfo?.smokeBarrier) {
const len = bendingInfo.smokeBarrier.w50?.[0]?.length ? String(bendingInfo.smokeBarrier.w50[0].length) : '3000';
products.push({
id: 'smoke-w50', category: code, productName: '연기차단재', productType: 'W50\n가이드레일용',
lengthDesign: len, widthDesign: '',
gapPoints: DEFAULT_GAP_PROFILES.smokeW50.map(p => ({ ...p, measured: '' })),
});
products.push({
id: 'smoke-w80', category: code, productName: '연기차단재', productType: 'W80\n케이스용',
lengthDesign: len, widthDesign: '',
gapPoints: DEFAULT_GAP_PROFILES.smokeW80.map(p => ({ ...p, measured: '' })),
});
} else if (!isStock) {
products.push({
id: 'smoke-w50', category: code, productName: '연기차단재', productType: 'W50\n가이드레일용',
lengthDesign: '3000', widthDesign: '',
gapPoints: DEFAULT_GAP_PROFILES.smokeW50.map(p => ({ ...p, measured: '' })),
});
products.push({
id: 'smoke-w80', category: code, productName: '연기차단재', productType: 'W80\n케이스용',
lengthDesign: '3000', widthDesign: '',
gapPoints: DEFAULT_GAP_PROFILES.smokeW80.map(p => ({ ...p, measured: '' })),
});
}
return products;
}
export const BendingInspectionContent = forwardRef<InspectionContentRef, BendingInspectionContentProps>(function BendingInspectionContent({
data: order,
@@ -147,34 +217,32 @@ export const BendingInspectionContent = forwardRef<InspectionContentRef, Bending
const today = getToday();
const { documentNo, primaryAssignee } = getOrderInfo(order);
const [products, setProducts] = useState<ProductRow[]>(() =>
INITIAL_PRODUCTS.map(p => ({
...p,
bendingStatus: null,
lengthMeasured: '',
widthMeasured: '',
gapPoints: p.gapPoints.map(gp => ({ ...gp })),
}))
);
const bendingInfo = order.bendingInfo as BendingInfoExtended | undefined;
const productCode = bendingInfo?.productCode || '-';
const finishMaterial = bendingInfo?.finishMaterial || '-';
const [products, setProducts] = useState<ProductRow[]>([]);
const [inadequateContent, setInadequateContent] = useState('');
// bendingInfo + 저장된 검사 데이터를 통합하여 products 생성
useEffect(() => {
if (!workItems || workItems.length === 0 || !inspectionDataMap) return;
// 1. bendingInfo 기반 templates 생성 (해당 부품만)
const templates = buildProductsFromBendingInfo(bendingInfo);
if (templates.length === 0) return;
// 검사 데이터가 있는 첫 번째 workItem 찾기 (workItems[0]에만 의존하지 않음)
// 2. 저장된 검사 데이터 찾기
let itemData: InspectionData | undefined;
for (const item of workItems) {
const data = inspectionDataMap.get(item.id);
if (data) {
itemData = data as unknown as InspectionData;
break;
if (workItems && inspectionDataMap) {
for (const item of workItems) {
const data = inspectionDataMap.get(item.id);
if (data) {
itemData = data as unknown as InspectionData;
break;
}
}
}
if (!itemData) return;
// 저장된 검사 성적서 포맷 (products 배열) 복원
const savedProducts = (itemData as unknown as Record<string, unknown>).products as Array<{
const savedProducts = (itemData as unknown as Record<string, unknown> | undefined)?.products as Array<{
id: string;
bendingStatus: CheckStatus;
lengthMeasured: string;
@@ -182,53 +250,35 @@ export const BendingInspectionContent = forwardRef<InspectionContentRef, Bending
gapPoints: Array<{ point: string; designValue: string; measured: string }>;
}> | undefined;
if (savedProducts && Array.isArray(savedProducts)) {
// 검사 성적서에서 저장된 전체 데이터 복원
setProducts(prev => prev.map(p => {
const saved = savedProducts.find(sp => sp.id === p.id);
if (!saved) return p;
return {
...p,
bendingStatus: saved.bendingStatus ?? p.bendingStatus,
lengthMeasured: saved.lengthMeasured || p.lengthMeasured,
widthMeasured: saved.widthMeasured || p.widthMeasured,
gapPoints: p.gapPoints.map((gp, gi) => ({
...gp,
measured: saved.gapPoints?.[gi]?.measured || gp.measured,
})),
};
}));
// 부적합 내용 복원
const savedInadequate = (itemData as unknown as Record<string, unknown>).inadequateContent;
if (typeof savedInadequate === 'string' && savedInadequate) {
setInadequateContent(savedInadequate);
}
return;
}
// 3. templates 기반으로 products 생성 + 저장 데이터 병합
const newProducts: ProductRow[] = templates.map(p => {
const saved = savedProducts?.find(sp => sp.id === p.id);
const inferredStatus: CheckStatus = itemData?.bendingStatus
? convertToCheckStatus(itemData.bendingStatus)
: itemData?.judgment === 'pass' ? '양호' : itemData?.judgment === 'fail' ? '불량' : null;
// 개소별 검사 입력 데이터에서 bendingStatus 로드
if (itemData.bendingStatus) {
const bendingStatusValue = convertToCheckStatus(itemData.bendingStatus);
setProducts(prev => prev.map(p => ({
return {
...p,
bendingStatus: bendingStatusValue,
})));
} else if (itemData.judgment) {
// 이전 형식 호환: products/bendingStatus 없이 judgment만 있는 경우
const inferredStatus: CheckStatus = itemData.judgment === 'pass' ? '양호' : itemData.judgment === 'fail' ? '불량' : null;
if (inferredStatus) {
setProducts(prev => prev.map(p => ({
...p,
bendingStatus: inferredStatus,
})));
}
}
bendingStatus: saved?.bendingStatus ?? inferredStatus ?? null,
lengthMeasured: saved?.lengthMeasured || '',
widthMeasured: saved?.widthMeasured || '',
gapPoints: p.gapPoints.map((gp, gi) => ({
...gp,
measured: saved?.gapPoints?.[gi]?.measured || '',
})),
};
});
// 부적합 내용 로드
if (itemData.nonConformingContent) {
setProducts(newProducts);
// 부적합 내용 복원
const savedInadequate = (itemData as unknown as Record<string, unknown> | undefined)?.inadequateContent;
if (typeof savedInadequate === 'string' && savedInadequate) {
setInadequateContent(savedInadequate);
} else if (itemData?.nonConformingContent) {
setInadequateContent(itemData.nonConformingContent);
}
}, [workItems, inspectionDataMap]);
}, [bendingInfo?.productCode, bendingInfo?.isStockProduction, workItems, inspectionDataMap]);
const handleStatusChange = useCallback((productId: string, value: CheckStatus) => {
if (readOnly) return;
@@ -291,7 +341,7 @@ export const BendingInspectionContent = forwardRef<InspectionContentRef, Bending
<tbody>
<tr>
<td className="border border-gray-400 bg-gray-50 px-3 py-2 font-medium w-24"></td>
<td className="border border-gray-400 px-3 py-2"></td>
<td className="border border-gray-400 px-3 py-2">{productCode}</td>
<td className="border border-gray-400 bg-gray-50 px-3 py-2 font-medium w-28"> LOT NO</td>
<td className="border border-gray-400 px-3 py-2">{order.lotNo || '-'}</td>
</tr>
@@ -315,9 +365,9 @@ export const BendingInspectionContent = forwardRef<InspectionContentRef, Bending
</tr>
<tr>
<td className="border border-gray-400 bg-gray-50 px-3 py-2 font-medium"></td>
<td className="border border-gray-400 px-3 py-2">KWE01</td>
<td className="border border-gray-400 px-3 py-2">{productCode}</td>
<td className="border border-gray-400 bg-gray-50 px-3 py-2 font-medium"></td>
<td className="border border-gray-400 px-3 py-2"></td>
<td className="border border-gray-400 px-3 py-2">{finishMaterial || '-'}</td>
</tr>
</tbody>
</table>

View File

@@ -14,6 +14,7 @@
import { useState, useCallback, useMemo, forwardRef, useImperativeHandle, useEffect } from 'react';
import type { WorkOrder } from '../types';
import type { BendingInfoExtended } from './bending/types';
import type { InspectionData } from '@/components/production/WorkerScreen/InspectionInputModal';
import type { WorkItemData } from '@/components/production/WorkerScreen/types';
import type { InspectionDataMap } from './InspectionReportModal';
@@ -51,6 +52,7 @@ export interface BendingWipInspectionContentProps {
interface InspectionRow {
id: number;
itemId?: string;
partName: string; // 검사부위: "가이드레일(측면) 본체(철재)"
productName: string;
processStatus: CheckStatus;
lengthDesign: string;
@@ -62,21 +64,42 @@ interface InspectionRow {
spacingMeasured: string;
}
function buildRow(i: number, order: WorkOrder, workItems?: WorkItemData[], inspectionDataMap?: InspectionDataMap): InspectionRow {
/** 품목명에서 길이(mm) 추출: "가이드레일(측면) 본체(철재) 2438mm" → "2438" */
function extractLengthFromItemName(name?: string): string {
if (!name) return '-';
const m = name.match(/(\d{3,5})\s*mm/i);
return m ? m[1] : '-';
}
function buildRow(i: number, order: WorkOrder, workItems?: WorkItemData[], inspectionDataMap?: InspectionDataMap, bendingInfo?: BendingInfoExtended): InspectionRow {
const item = workItems?.[i];
const orderItem = order.items?.[i];
const itemData = item && inspectionDataMap?.get(item.id);
const itemName = item?.itemName || orderItem?.productName || '';
// 실제 길이: 품목명에서 추출 → bendingInfo에서 추출 → 폴백
let lengthDesign = extractLengthFromItemName(itemName);
if (lengthDesign === '-' && bendingInfo?.guideRail) {
const data = bendingInfo.guideRail.side || bendingInfo.guideRail.wall;
const len = data?.lengthData?.[0]?.length;
if (len) lengthDesign = String(len);
}
// 검사부위: 품목명에서 길이(mm) 부분 제거 → "가이드레일(측면) 본체(철재)"
const partName = itemName.replace(/\s*\d{3,5}\s*mm\s*/i, '').trim();
return {
id: i + 1,
itemId: item?.id,
productName: item?.itemName || orderItem?.productName || '',
partName,
productName: itemName,
processStatus: itemData ? convertToCheckStatus(itemData.bendingStatus) : null,
lengthDesign: '4000',
lengthDesign,
lengthMeasured: '',
widthDesign: 'N/A',
widthMeasured: 'N/A',
spacingPoint: '',
spacingDesign: '380',
spacingDesign: '-',
spacingMeasured: '',
};
}
@@ -94,18 +117,19 @@ export const BendingWipInspectionContent = forwardRef<InspectionContentRef, Bend
const today = getToday();
const { documentNo, primaryAssignee } = getOrderInfo(order);
const rowCount = workItems?.length || order.items?.length || DEFAULT_ROW_COUNT;
const bendingInfo = order.bendingInfo as BendingInfoExtended | undefined;
const [rows, setRows] = useState<InspectionRow[]>(() =>
Array.from({ length: rowCount }, (_, i) => buildRow(i, order, workItems, inspectionDataMap))
);
// 실제 품목 수만큼만 행 생성 (빈 행 제거)
const actualItemCount = order.items?.filter(item => item.productName)?.length || 0;
const rowCount = actualItemCount || 1;
const [rows, setRows] = useState<InspectionRow[]>([]);
const [inadequateContent, setInadequateContent] = useState('');
useEffect(() => {
const newRowCount = workItems?.length || order.items?.length || DEFAULT_ROW_COUNT;
setRows(Array.from({ length: newRowCount }, (_, i) => buildRow(i, order, workItems, inspectionDataMap)));
}, [workItems, inspectionDataMap, order.items]);
const count = order.items?.filter(item => item.productName)?.length || 1;
setRows(Array.from({ length: count }, (_, i) => buildRow(i, order, workItems, inspectionDataMap, bendingInfo)));
}, [workItems, inspectionDataMap, order.items, bendingInfo?.productCode]);
const handleStatusChange = useCallback((rowId: number, value: CheckStatus) => {
if (readOnly) return;
@@ -172,7 +196,10 @@ export const BendingWipInspectionContent = forwardRef<InspectionContentRef, Bend
</tr>
<tr>
<td className="border border-gray-400 bg-gray-50 px-3 py-2 font-medium"></td>
<td className="border border-gray-400 px-3 py-2">3,000 mm</td>
<td className="border border-gray-400 px-3 py-2">{(() => {
const len = extractLengthFromItemName(order.items?.[0]?.productName);
return len !== '-' ? `${Number(len).toLocaleString()} mm` : '-';
})()}</td>
<td className="border border-gray-400 bg-gray-50 px-3 py-2 font-medium"></td>
<td className="border border-gray-400 px-3 py-2">{order.items?.reduce((sum, item) => sum + item.quantity, 0) || 0} EA</td>
</tr>
@@ -243,7 +270,7 @@ export const BendingWipInspectionContent = forwardRef<InspectionContentRef, Bend
<thead>
<tr className="bg-gray-100">
<th className="border border-gray-400 p-1 w-8" rowSpan={2}>No.</th>
<th className="border border-gray-400 p-1 w-20" rowSpan={2}></th>
<th className="border border-gray-400 p-1" rowSpan={2}> </th>
<th className="border border-gray-400 p-1 w-16" rowSpan={2}><br/></th>
<th className="border border-gray-400 p-1" colSpan={2}> (mm)</th>
<th className="border border-gray-400 p-1" colSpan={2}> (mm)</th>
@@ -266,9 +293,7 @@ export const BendingWipInspectionContent = forwardRef<InspectionContentRef, Bend
return (
<tr key={row.id}>
<td className="border border-gray-400 p-1 text-center">{row.id}</td>
<td className="border border-gray-400 p-1">
<input type="text" value={row.productName} onChange={(e) => handleInputChange(row.id, 'productName', e.target.value)} disabled={readOnly} className={INPUT_CLASS} placeholder="-" />
</td>
<td className="border border-gray-400 p-1">{row.partName || '-'}</td>
<CheckStatusCell value={row.processStatus} onToggle={(v) => handleStatusChange(row.id, v)} readOnly={readOnly} />
<td className="border border-gray-400 p-1 text-center">{row.lengthDesign}</td>
<td className="border border-gray-400 p-1">

View File

@@ -17,6 +17,7 @@
* - 생산량 합계 [kg] SUS/EGI
*/
import { useMemo } from 'react';
import type { WorkOrder } from '../types';
import type { BendingInfoExtended } from './bending/types';
import { ConstructionApprovalTable } from '@/components/document-system';
@@ -31,9 +32,10 @@ interface BendingWorkLogContentProps {
data: WorkOrder;
lotNoMap?: Record<string, string>; // BD-{prefix}-{lengthCode} → LOT NO
bendingImages?: Record<string, string>; // R2 presigned URL 맵
rawMaterialLotNo?: string; // STOCK: 원자재 투입 LOT 번호
}
export function BendingWorkLogContent({ data: order, lotNoMap, bendingImages }: BendingWorkLogContentProps) {
export function BendingWorkLogContent({ data: order, lotNoMap, bendingImages, rawMaterialLotNo }: BendingWorkLogContentProps) {
const today = new Date().toLocaleDateString('ko-KR', {
year: 'numeric',
month: '2-digit',
@@ -74,6 +76,35 @@ export function BendingWorkLogContent({ data: order, lotNoMap, bendingImages }:
? calculateProductionSummary(bendingInfo!, mapping)
: { susTotal: 0, egiTotal: 0, grandTotal: 0 };
// STOCK 단일부품: 원자재 LOT를 모든 부품에 적용
const effectiveLotNoMap = useMemo(() => {
if (!rawMaterialLotNo || !bendingInfo?.isStockProduction) return lotNoMap;
const map = { ...lotNoMap };
// stockPartFilter의 각 부품에 원자재 LOT 번호 매핑
const stockFilter = bendingInfo.stockPartFilter;
if (stockFilter) {
for (const part of stockFilter) {
// partKey별 lotPrefix 매핑
const prefixMap: Record<string, string[]> = {
'본체': ['ST', 'SM', 'RT', 'RM'],
'C형': ['SC', 'RC'],
'D형': ['SD', 'RD'],
'마감재': ['SS', 'SE', 'RS', 'RE'],
'별도마감': ['YY'],
'BASE': ['XX'],
};
const prefixes = prefixMap[part.partKey] || [];
for (const prefix of prefixes) {
// 와일드카드: BD-{prefix}-* 형태로 모든 길이에 매핑
map[`BD-${prefix}-*`] = rawMaterialLotNo;
}
}
}
// 와일드카드 없이도 fallback으로 작동하도록 _raw 키 추가
map['_rawMaterialLot'] = rawMaterialLotNo;
return map;
}, [lotNoMap, rawMaterialLotNo, bendingInfo]);
return (
<div className="p-6 bg-white">
{/* ===== 헤더 영역 ===== */}
@@ -166,23 +197,23 @@ export function BendingWorkLogContent({ data: order, lotNoMap, bendingImages }:
bendingInfo={bendingInfo!}
mapping={mapping}
lotNo={order.lotNo}
lotNoMap={lotNoMap}
lotNoMap={effectiveLotNoMap}
bendingImages={bendingImages}
/>
<BottomBarSection
bendingInfo={bendingInfo!}
mapping={mapping}
lotNoMap={lotNoMap}
lotNoMap={effectiveLotNoMap}
bendingImages={bendingImages}
/>
<ShutterBoxSection
bendingInfo={bendingInfo!}
lotNoMap={lotNoMap}
lotNoMap={effectiveLotNoMap}
bendingImages={bendingImages}
/>
<SmokeBarrierSection
bendingInfo={bendingInfo!}
lotNoMap={lotNoMap}
lotNoMap={effectiveLotNoMap}
bendingImages={bendingImages}
/>
</>

View File

@@ -400,14 +400,20 @@ export const TemplateInspectionContent = forwardRef<InspectionContentRef, Templa
const bi = order.bendingInfo as BendingInfoExtended | undefined;
const wallLen = bi?.guideRail?.wall?.lengthData?.[0]?.length;
const sideLen = bi?.guideRail?.side?.lengthData?.[0]?.length;
// 품목명에서 길이 추출 (예: "가이드레일(측면) 본체(철재) 2438mm" → 2438)
const itemLen = order.items?.[0]?.productName?.match(/(\d{3,5})\s*mm/i)?.[1];
// 실제 길이: bendingInfo > 품목명 > 기본값
const actualWallLen = String(wallLen || itemLen || 3500);
const actualSideLen = String(sideLen || itemLen || 3000);
const actualLen = itemLen || '3000';
return inspectionConfig.items.map((item): BendingProduct => {
// API id → 표시용 매핑 (이름, 타입, 치수)
const displayMap: Record<string, { name: string; type: string; len: string; wid: string }> = {
guide_rail_wall: { name: '가이드레일', type: '벽면형', len: String(wallLen || 3500), wid: 'N/A' },
guide_rail_side: { name: '가이드레일', type: '측면형', len: String(sideLen || 3000), wid: 'N/A' },
bottom_bar: { name: '하단마감재', type: '60×40', len: '3000', wid: 'N/A' },
case_box: { name: '케이스', type: '양면', len: '3000', wid: 'N/A' },
guide_rail_wall: { name: '가이드레일', type: '벽면형', len: actualWallLen, wid: 'N/A' },
guide_rail_side: { name: '가이드레일', type: '측면형', len: actualSideLen, wid: 'N/A' },
bottom_bar: { name: '하단마감재', type: '60×40', len: actualLen, wid: 'N/A' },
case_box: { name: '케이스', type: '양면', len: actualLen, wid: 'N/A' },
smoke_w50: { name: '연기차단재', type: '화이바 W50\n가이드레일용', len: '-', wid: '50' },
smoke_w80: { name: '연기차단재', type: '화이바 W80\n케이스용', len: '-', wid: '80' },
};
@@ -1348,8 +1354,8 @@ export const TemplateInspectionContent = forwardRef<InspectionContentRef, Templa
const cell = cellValues[cellKey];
const label = col.label.trim();
// 1. 분류/제품명 (text)
if (col.column_type === 'text' && (label.includes('분류') || label.includes('제품명'))) {
// 1. 분류/제품명/검사부위 (text)
if (col.column_type === 'text' && (label.includes('분류') || label.includes('제품명') || label.includes('부위'))) {
return (
<td key={col.id} rowSpan={rs}
className="border border-gray-400 px-2 py-1.5 text-center align-middle text-xs"

View File

@@ -78,17 +78,37 @@ function PartTable({ title, rows, imageUrl, lotNo: _lotNo, baseSize, lotNoMap }:
}
export function GuideRailSection({ bendingInfo, mapping, lotNo, lotNoMap, bendingImages }: GuideRailSectionProps) {
const { wall, side } = bendingInfo.guideRail;
const guideRail = bendingInfo.guideRail;
if (!guideRail) return null;
const { wall, side } = guideRail;
const productCode = bendingInfo.productCode;
const stockFilter = bendingInfo.stockPartFilter;
const wallRows = wall
let wallRows = wall
? buildWallGuideRailRows(wall.lengthData, wall.baseDimension || '135*80', mapping, productCode)
: [];
const sideRows = side
let sideRows = side
? buildSideGuideRailRows(side.lengthData, side.baseDimension || '135*130', mapping, productCode)
: [];
// STOCK 단일부품 생산: stockPartFilter에 해당하는 부품만 표시
if (stockFilter && stockFilter.length > 0) {
const allowedKeys = new Set(stockFilter.map(f => f.partKey));
const filterRows = (rows: typeof wallRows) =>
rows.filter(row => {
if (allowedKeys.has('본체') && row.partName.includes('본체')) return true;
if (allowedKeys.has('C형') && row.partName.includes('C형')) return true;
if (allowedKeys.has('D형') && row.partName.includes('D형')) return true;
if (allowedKeys.has('마감재') && row.partName.includes('마감')) return true;
if (allowedKeys.has('별도마감') && row.partName.includes('별도마감')) return true;
if (allowedKeys.has('BASE') && row.partName.includes('BASE')) return true;
return false;
});
wallRows = filterRows(wallRows);
sideRows = filterRows(sideRows);
}
if (wallRows.length === 0 && sideRows.length === 0) return null;
return (

View File

@@ -66,6 +66,13 @@ export interface BendingInfoExtended {
w50: LengthQuantity[]; // 레일용 W50
w80Qty: number; // 케이스용 W80 수량
};
// STOCK 재고생산 전용
isStockProduction?: boolean;
stockPartFilter?: Array<{
itemName: string; // "가이드레일(측면) 본체(철재) 2438mm"
partKey: string; // "본체", "C형", "D형", "마감재" 등
}>;
}
// 재질 매핑 결과

View File

@@ -342,6 +342,7 @@ export function buildBottomBarRows(
productCode?: string,
): BottomBarPartRow[] {
const rows: BottomBarPartRow[] = [];
if (!bottomBar) return rows;
const codePrefix = productCode?.replace(/\d+$/, '') || 'KSS';
const isSteel = codePrefix === 'KTE';
const lotPrefix = isSteel ? 'TS' : (mapping.bottomBarFinish.includes('SUS') ? 'BS' : 'BE');
@@ -488,9 +489,10 @@ export function buildSmokeBarrierRows(
smokeBarrier: BendingInfoExtended['smokeBarrier'],
): SmokeBarrierPartRow[] {
const rows: SmokeBarrierPartRow[] = [];
if (!smokeBarrier) return rows;
// 레일용 W50
for (const ld of smokeBarrier.w50) {
for (const ld of (smokeBarrier.w50 ?? [])) {
if (ld.quantity <= 0) continue;
const w = calcWeight('EGI 0.8T', SMOKE_BARRIER_WIDTH, ld.length);
const code = getSLengthCode(ld.length, '연기차단재50');
@@ -503,7 +505,7 @@ export function buildSmokeBarrierRows(
}
// 케이스용 W80
if (smokeBarrier.w80Qty > 0) {
if ((smokeBarrier.w80Qty ?? 0) > 0) {
const w = calcWeight('EGI 0.8T', SMOKE_BARRIER_WIDTH, 3000);
const code = getSLengthCode(3000, '연기차단재80');
rows.push({
@@ -537,7 +539,7 @@ export function calculateProductionSummary(
}
// 가이드레일 - 벽면형
if (bendingInfo.guideRail.wall) {
if (bendingInfo.guideRail?.wall) {
for (const ld of bendingInfo.guideRail.wall.lengthData) {
if (ld.quantity <= 0) continue;
addWeight(mapping.guideRailFinish, WALL_PART_WIDTH, ld.length, ld.quantity);
@@ -552,7 +554,7 @@ export function calculateProductionSummary(
}
// 가이드레일 - 측면형
if (bendingInfo.guideRail.side) {
if (bendingInfo.guideRail?.side) {
for (const ld of bendingInfo.guideRail.side.lengthData) {
if (ld.quantity <= 0) continue;
addWeight(mapping.guideRailFinish, SIDE_PART_WIDTH, ld.length, ld.quantity);
@@ -564,13 +566,13 @@ export function calculateProductionSummary(
}
// 하단마감재
if (bendingInfo.bottomBar.length3000Qty > 0) {
if (bendingInfo.bottomBar?.length3000Qty > 0) {
addWeight(mapping.bottomBarFinish, BOTTOM_BAR_WIDTH, 3000, bendingInfo.bottomBar.length3000Qty);
if (mapping.bottomBarExtraFinish !== '없음' && mapping.bottomBarExtraFinish) {
addWeight(mapping.bottomBarExtraFinish, EXTRA_FINISH_WIDTH, 3000, bendingInfo.bottomBar.length3000Qty);
}
}
if (bendingInfo.bottomBar.length4000Qty > 0) {
if (bendingInfo.bottomBar?.length4000Qty > 0) {
addWeight(mapping.bottomBarFinish, BOTTOM_BAR_WIDTH, 4000, bendingInfo.bottomBar.length4000Qty);
if (mapping.bottomBarExtraFinish !== '없음' && mapping.bottomBarExtraFinish) {
addWeight(mapping.bottomBarExtraFinish, EXTRA_FINISH_WIDTH, 4000, bendingInfo.bottomBar.length4000Qty);
@@ -578,7 +580,7 @@ export function calculateProductionSummary(
}
// 셔터박스
for (const box of bendingInfo.shutterBox) {
for (const box of (bendingInfo.shutterBox ?? [])) {
const boxRows = buildShutterBoxRows(box);
for (const row of boxRows) {
if (row.weight > 0) egiTotal += row.weight; // 셔터박스는 항상 EGI
@@ -645,5 +647,10 @@ export function lookupLotNo(
// 2. Fallback: prefix만으로 매칭 (첫 번째 일치 항목)
const prefixKey = `BD-${prefix}-`;
const fallbackKey = Object.keys(lotNoMap).find(k => k.startsWith(prefixKey));
return fallbackKey ? lotNoMap[fallbackKey] : '-';
if (fallbackKey) return lotNoMap[fallbackKey];
// 3. STOCK 원자재 LOT fallback (와일드카드 또는 _rawMaterialLot)
const wildcard = lotNoMap[`BD-${prefix}-*`];
if (wildcard) return wildcard;
return lotNoMap['_rawMaterialLot'] || '-';
}

View File

@@ -485,7 +485,7 @@ export function transformApiToFrontend(api: WorkOrderApi): WorkOrder {
return {
id: String(api.id),
workOrderNo: api.work_order_no,
lotNo: api.sales_order?.order_no || '-',
lotNo: api.sales_order?.options?.bending_lot?.lot_number || api.sales_order?.order_no || '-',
processId: api.process_id,
processName: api.process?.process_name || '-',
processCode: api.process?.process_code || '-',

View File

@@ -547,8 +547,9 @@ export function InspectionInputModal({
workOrderId,
}: InspectionInputModalProps) {
// 템플릿 모드 여부
// 절곡(bending)은 7제품 커스텀 폼 사용 → TemplateInspectionContent의 bending 셀 키와 연동
const useTemplateMode = processType !== 'bending' && !!(templateData?.has_template && templateData.template);
// 절곡(bending/bending_wip)은 7제품 커스텀 폼 사용 → TemplateInspectionContent의 bending 셀 키와 연동
const isBendingProcess = processType === 'bending' || processType === 'bending_wip';
const useTemplateMode = !isBendingProcess && !!(templateData?.has_template && templateData.template);
const [formData, setFormData] = useState<InspectionData>({
productName,
@@ -577,19 +578,21 @@ export function InspectionInputModal({
// API에서 절곡 제품 gap_points 동적 로딩
useEffect(() => {
if (!open || processType !== 'bending' || !workOrderId) return;
if (!open || !isBendingProcess || !workOrderId) return;
let cancelled = false;
getInspectionConfig(workOrderId).then(result => {
if (cancelled) return;
if (result.success && result.data?.items?.length) {
// 실제 품목 길이: workItemDimensions.height (예: 2438mm) 우선, 없으면 3000 폴백
const actualLen = workItemDimensions?.height ? String(workItemDimensions.height) : '3000';
const displayMap: Record<string, { label: string; len: string; wid: string }> = {
guide_rail_wall: { label: '가이드레일 (벽면형)', len: '3000', wid: 'N/A' },
guide_rail_side: { label: '가이드레일 (측면형)', len: '3000', wid: 'N/A' },
case_box: { label: '케이스 (500X380)', len: '3000', wid: 'N/A' },
bottom_bar: { label: '하단마감재 (60X40)', len: '3000', wid: 'N/A' },
bottom_l_bar: { label: '하단L-BAR (17X60)', len: '3000', wid: 'N/A' },
smoke_w50: { label: '연기차단재 (W50)', len: '3000', wid: '' },
smoke_w80: { label: '연기차단재 (W80)', len: '3000', wid: '' },
guide_rail_wall: { label: '가이드레일 (벽면형)', len: actualLen, wid: 'N/A' },
guide_rail_side: { label: '가이드레일 (측면형)', len: actualLen, wid: 'N/A' },
case_box: { label: '케이스 (500X380)', len: actualLen, wid: 'N/A' },
bottom_bar: { label: '하단마감재 (60X40)', len: actualLen, wid: 'N/A' },
bottom_l_bar: { label: '하단L-BAR (17X60)', len: actualLen, wid: 'N/A' },
smoke_w50: { label: '연기차단재 (W50)', len: actualLen, wid: '' },
smoke_w80: { label: '연기차단재 (W80)', len: actualLen, wid: '' },
};
const defs: BendingProductDef[] = result.data.items.map(item => {
const d = displayMap[item.id] || { label: item.name, len: '-', wid: 'N/A' };
@@ -605,11 +608,11 @@ export function InspectionInputModal({
}
});
return () => { cancelled = true; };
}, [open, processType, workOrderId]);
}, [open, processType, workOrderId, workItemDimensions?.height]);
// API 제품 정의 로딩 시 bendingProducts 갱신 (gap 개수 동기화)
useEffect(() => {
if (!apiProductDefs || processType !== 'bending') return;
if (!apiProductDefs || !isBendingProcess) return;
setBendingProducts(prev => {
return apiProductDefs.map((def, idx) => {
// 기존 입력값 보존 (ID 매칭 또는 인덱스 폴백)
@@ -663,7 +666,7 @@ export function InspectionInputModal({
gapMeasured: def.gapPoints.map((_, gi) => saved.gapPoints?.[gi]?.measured || ''),
};
}));
} else if (processType === 'bending' && initialData.judgment) {
} else if (isBendingProcess && initialData.judgment) {
// 이전 형식 데이터 호환: products 배열 없이 저장된 경우
// judgment 값으로 제품별 상태 추론 (pass → 전체 양호)
const restoredStatus: 'good' | 'bad' | null =
@@ -751,7 +754,7 @@ export function InspectionInputModal({
return computeDynamicJudgment(templateData.template, dynamicFormValues, workItemDimensions);
}
// 절곡 7개 제품 전용 판정
if (processType === 'bending') {
if (isBendingProcess) {
let allGood = true;
let allFilled = true;
for (const p of bendingProducts) {
@@ -785,7 +788,7 @@ export function InspectionInputModal({
};
// 절곡: products 배열을 성적서와 동일 포맷으로 저장
if (processType === 'bending') {
if (isBendingProcess) {
const products = bendingProducts.map((p, idx) => ({
id: p.id,
bendingStatus: p.bendingStatus === 'good' ? '양호' : p.bendingStatus === 'bad' ? '불량' : null,
@@ -841,22 +844,55 @@ export function InspectionInputModal({
)}
onClick={() => {
if (!formData.judgment) {
const w = workItemDimensions?.width || 1000;
const h = workItemDimensions?.height || 500;
setFormData((prev) => ({
...prev,
bendingStatus: 'good',
processingStatus: 'good',
sewingStatus: 'good',
assemblyStatus: 'good',
length: w,
width: h,
height1: h,
height2: h,
judgment: 'pass',
nonConformingContent: '',
}));
// 동적 템플릿 모드: 각 항목의 기준값을 사용하여 적합한 값 입력
if (useTemplateMode && templateData?.template) {
const testValues: Record<string, unknown> = {};
for (const section of templateData.template.sections) {
for (const item of section.items) {
const fieldKey = `section_${section.id}_item_${item.id}`;
if (isNumericItem(item)) {
const design = resolveDesignValue(item, workItemDimensions);
testValues[fieldKey] = design ?? 100;
} else {
testValues[fieldKey] = 'ok';
}
}
}
setDynamicFormValues(testValues);
}
if (!useTemplateMode) {
// 레거시 모드: 기존 로직
const w = workItemDimensions?.width || 1000;
const h = workItemDimensions?.height || 500;
setFormData((prev) => ({
...prev,
bendingStatus: 'good',
processingStatus: 'good',
sewingStatus: 'good',
assemblyStatus: 'good',
length: w,
width: h,
height1: h,
height2: h,
judgment: 'pass',
nonConformingContent: '',
}));
// 절곡 7제품: 모든 제품 양호 + 도면치수와 동일한 측정값 입력
if (isBendingProcess) {
setBendingProducts(effectiveProductDefs.map(def => ({
id: def.id,
bendingStatus: 'good' as const,
lengthMeasured: def.lengthDesign || '',
widthMeasured: def.widthDesign || '',
gapMeasured: def.gapPoints.map(gp => gp.design || ''),
})));
}
}
} else {
// 초기화
if (useTemplateMode) {
setDynamicFormValues({});
}
setFormData((prev) => ({
...prev,
bendingStatus: null,
@@ -870,6 +906,10 @@ export function InspectionInputModal({
judgment: null,
nonConformingContent: '',
}));
// 절곡 7제품 초기화
if (isBendingProcess) {
setBendingProducts(createInitialBendingProducts());
}
}
}}
>
@@ -914,8 +954,8 @@ export function InspectionInputModal({
{/* ===== 레거시: 공정별 하드코딩 검사 항목 (템플릿 없을 때만 표시) ===== */}
{/* ===== 재고생산 (bending_wip) 검사 항목 ===== */}
{!useTemplateMode && processType === 'bending_wip' && (
{/* ===== 재고생산 (bending_wip) 검사 항목 — 7제품 폼으로 통합됨 (위 절곡 검사 항목 참조) ===== */}
{false && processType === 'bending_wip' && (
<>
<div className="space-y-1.5">
<span className="text-sm font-bold"> </span>
@@ -1139,7 +1179,7 @@ export function InspectionInputModal({
)}
{/* ===== 절곡 검사 항목 (7개 제품별) ===== */}
{!useTemplateMode && processType === 'bending' && (
{!useTemplateMode && isBendingProcess && (
<div className="space-y-4">
{effectiveProductDefs.map((productDef, pIdx) => {
const pState = bendingProducts[pIdx];

View File

@@ -22,6 +22,7 @@ function extractLengthFromName(name?: string | null): number {
import { useState, useMemo, useCallback, useEffect } from 'react';
import dynamic from 'next/dynamic';
import { useSearchParams, useRouter, usePathname } from 'next/navigation';
import { useSidebarCollapsed } from '@/stores/menuStore';
import { ClipboardList, PlayCircle, CheckCircle2, AlertTriangle, ChevronDown, ChevronUp, List } from 'lucide-react';
import {
@@ -145,9 +146,20 @@ const PROCESS_STEPS: Record<string, { name: string; isMaterialInput: boolean; is
export default function WorkerScreen() {
// ===== 상태 관리 =====
const sidebarCollapsed = useSidebarCollapsed();
const searchParams = useSearchParams();
const router = useRouter();
const pathname = usePathname();
const [workOrders, setWorkOrders] = useState<WorkOrder[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [activeTab, setActiveTab] = useState<string>('');
const [activeTab, setActiveTabState] = useState<string>(searchParams.get('tab') || '');
// 탭 변경 시 URL query parameter 동기화 (새로고침 시 탭 유지)
const setActiveTab = useCallback((tab: string) => {
setActiveTabState(tab);
const params = new URLSearchParams(searchParams.toString());
params.set('tab', tab);
router.replace(`${pathname}?${params.toString()}`, { scroll: false });
}, [searchParams, router, pathname]);
const [bendingSubMode, setBendingSubMode] = useState<'normal' | 'wip'>('normal');
const [slatSubMode, setSlatSubMode] = useState<'normal' | 'jointbar'>('normal');
@@ -328,14 +340,21 @@ export default function WorkerScreen() {
return groupedTabs.find((g) => g.group === groupName) || null;
}, [activeTab, processListCache, groupedTabs]);
// 공정 목록 로드 후 첫 번째 그룹을 기본 선택
// 공정 목록 로드 후 탭 선택 (URL 파라미터 우선, 없으면 첫 번째 그룹)
useEffect(() => {
if (activeTab) return;
if (groupedTabs.length > 0) {
setActiveTab(groupedTabs[0].defaultProcessId);
} else if (!isLoading) {
setActiveTab('screen');
if (groupedTabs.length === 0 && !isLoading) {
if (!activeTab) setActiveTabState('screen');
return;
}
if (groupedTabs.length === 0) return;
// URL에 tab이 있고 유효한 탭이면 유지
if (activeTab) {
const isValid = groupedTabs.some((g) => g.defaultProcessId === activeTab);
if (isValid) return;
}
// 없으면 첫 번째 그룹 선택
setActiveTabState(groupedTabs[0].defaultProcessId);
}, [groupedTabs, activeTab, isLoading]);
// 선택된 공정의 ProcessTab 키 (mock 데이터 및 기존 로직 호환용)
@@ -350,15 +369,19 @@ export default function WorkerScreen() {
}, [activeTab, processListCache]);
// 선택된 공정의 작업일지/검사성적서 설정
// subProcessId가 선택되어 있으면 자식 공정의 설정 사용
const activeProcessSettings = useMemo(() => {
const process = processListCache.find((p) => p.id === activeTab);
const effectiveId = subProcessId !== 'all' ? subProcessId : activeTab;
const process = processListCache.find((p) => p.id === effectiveId);
// 자식 공정에 설정이 없으면 부모 공정 폴백
const parentProcess = processListCache.find((p) => p.id === activeTab);
return {
needsWorkLog: process?.needsWorkLog ?? false,
hasDocumentTemplate: !!process?.documentTemplateId,
workLogTemplateId: process?.workLogTemplateId,
workLogTemplateName: process?.workLogTemplateName,
needsWorkLog: process?.needsWorkLog ?? parentProcess?.needsWorkLog ?? false,
hasDocumentTemplate: !!(process?.documentTemplateId ?? parentProcess?.documentTemplateId),
workLogTemplateId: process?.workLogTemplateId ?? parentProcess?.workLogTemplateId,
workLogTemplateName: process?.workLogTemplateName ?? parentProcess?.workLogTemplateName,
};
}, [activeTab, processListCache]);
}, [activeTab, subProcessId, processListCache]);
// activeTab 변경 시 해당 공정의 중간검사 설정 조회
useEffect(() => {
@@ -1329,8 +1352,13 @@ export default function WorkerScreen() {
// ===== 재공품 감지 =====
const hasWipItems = useMemo(() => {
return activeProcessTabKey === 'bending' && workItems.some(item => item.isWip);
}, [activeProcessTabKey, workItems]);
if (activeProcessTabKey !== 'bending') return false;
// 1. workItems에서 isWip 체크
if (workItems.some(item => item.isWip)) return true;
// 2. Fallback: 선택된 작업지시의 프로젝트명/수주번호로 WIP 판별
const selectedWo = filteredWorkOrders.find(wo => wo.id === selectedSidebarOrderId);
return !!(selectedWo && (selectedWo.projectName === '재고생산' || selectedWo.salesOrderNo?.startsWith('STK')));
}, [activeProcessTabKey, workItems, filteredWorkOrders, selectedSidebarOrderId]);
// ===== 조인트바 감지 =====
const hasJointBarItems = useMemo(() => {
@@ -1620,33 +1648,22 @@ export default function WorkerScreen() {
{(hasWipItems || activeProcessSettings.needsWorkLog || activeProcessSettings.hasDocumentTemplate) && (
<div className={`fixed bottom-4 left-4 right-4 px-4 py-3 bg-background/95 backdrop-blur rounded-xl border shadow-lg z-50 transition-all duration-300 md:bottom-6 md:px-6 md:right-[24px] ${sidebarCollapsed ? 'md:left-[120px]' : 'md:left-[280px]'}`}>
<div className="flex gap-2 md:gap-3">
{hasWipItems ? (
{(hasWipItems || activeProcessSettings.needsWorkLog) && (
<Button
variant="outline"
onClick={handleWorkLog}
className="flex-1 py-5 md:py-6 text-sm md:text-base font-medium"
>
</Button>
)}
{(hasWipItems || activeProcessSettings.hasDocumentTemplate) && (
<Button
onClick={handleInspection}
className="flex-1 py-5 md:py-6 text-sm md:text-base font-medium bg-gray-900 hover:bg-gray-800"
>
</Button>
) : (
<>
{activeProcessSettings.needsWorkLog && (
<Button
variant="outline"
onClick={handleWorkLog}
className="flex-1 py-5 md:py-6 text-sm md:text-base font-medium"
>
</Button>
)}
{activeProcessSettings.hasDocumentTemplate && (
<Button
onClick={handleInspection}
className="flex-1 py-5 md:py-6 text-sm md:text-base font-medium bg-gray-900 hover:bg-gray-800"
>
</Button>
)}
</>
)}
</div>
</div>