fix(WEB): 검사성적서 보기 레이아웃 및 데이터 폴백 개선

- TemplateInspectionContent: 푸터 비고/종합판정 높이 동일 배치, 판정 표시 간소화
- InspectionReportModal: props 데이터 비어있을 때 API 로딩 데이터 폴백 처리
This commit is contained in:
2026-02-11 15:58:52 +09:00
parent 911b6ca31a
commit d1e805a88d
2 changed files with 77 additions and 32 deletions

View File

@@ -135,9 +135,10 @@ export function InspectionReportModal({
const [selfTemplateData, setSelfTemplateData] = useState<InspectionTemplateData | null>(null);
// props에서 목업 제외한 실제 개소만 사용 (WorkerScreen에서 apiItems + mockItems가 합쳐져 전달됨)
// React에서는 개소 미등록 시 성적서 버튼 자체가 노출되지 않으므로 API fallback 불필요
const effectiveWorkItems = propWorkItems?.filter(w => !w.id.startsWith('mock-'));
const effectiveInspectionDataMap = propInspectionDataMap;
// props 데이터가 없거나 비어있으면 API 로딩 데이터를 fallback으로 사용
const propFiltered = propWorkItems?.filter(w => !w.id.startsWith('mock-'));
const effectiveWorkItems = (propFiltered && propFiltered.length > 0) ? propFiltered : apiWorkItems ?? undefined;
const effectiveInspectionDataMap = (propInspectionDataMap && propInspectionDataMap.size > 0) ? propInspectionDataMap : apiInspectionDataMap ?? undefined;
// 목업 WorkOrder 생성
const createMockOrder = (id: string, pType: ProcessType): WorkOrder => ({

View File

@@ -156,6 +156,17 @@ export const TemplateInspectionContent = forwardRef<InspectionContentRef, Templa
[dataSections]
);
// sectionItem.id → section.id 역매핑
const itemSectionMap = useMemo(() => {
const map = new Map<number, number>();
for (const s of dataSections) {
for (const item of s.items) {
map.set(item.id, s.id);
}
}
return map;
}, [dataSections]);
// 컬럼 → 섹션 아이템 매핑 (라벨 정규화 비교)
const columnItemMap = useMemo(() => {
const map = new Map<number, InspectionTemplateSectionItem>();
@@ -183,6 +194,8 @@ export const TemplateInspectionContent = forwardRef<InspectionContentRef, Templa
const effectiveWorkItems = workItems || [];
// inspectionDataMap에서 초기값 복원
// InspectionInputModal 저장 키: section_{sectionId}_item_{itemId} / 값: "ok"|"ng"|number
// TemplateInspectionContent 내부 키: {rowIdx}-{colId} / 값: CellValue
useEffect(() => {
if (!inspectionDataMap || !workItems) return;
const initial: Record<string, CellValue> = {};
@@ -192,14 +205,54 @@ export const TemplateInspectionContent = forwardRef<InspectionContentRef, Templa
for (const col of template.columns) {
const sectionItem = columnItemMap.get(col.id);
if (!sectionItem) continue;
const key = `${rowIdx}-${col.id}`;
const val = itemData.templateValues?.[`item_${sectionItem.id}`];
if (val && typeof val === 'object') {
initial[key] = val as CellValue;
const cellKey = `${rowIdx}-${col.id}`;
const sectionId = itemSectionMap.get(sectionItem.id);
// InspectionInputModal 키 형식으로 조회
const inputKey = sectionId != null
? `section_${sectionId}_item_${sectionItem.id}`
: null;
// 레거시 키 형식 폴백
const legacyKey = `item_${sectionItem.id}`;
const rawVal = (inputKey ? itemData.templateValues?.[inputKey] : undefined)
?? itemData.templateValues?.[legacyKey];
if (rawVal == null) continue;
// 값 형식 변환: InputModal 형식 → CellValue 형식
// complex 컬럼(측정값)은 measurements 배열, check 컬럼은 status, 나머지는 value
const isComplex = col.column_type === 'complex' && col.sub_labels && col.sub_labels.length > 0;
if (typeof rawVal === 'object') {
// 이미 CellValue 객체
initial[cellKey] = rawVal as CellValue;
} else if (rawVal === 'ok') {
initial[cellKey] = { status: 'good' };
} else if (rawVal === 'ng') {
initial[cellKey] = { status: 'bad' };
} else if (isComplex) {
// complex 컬럼: 숫자/문자열을 첫 번째 측정값으로 매핑
const strVal = typeof rawVal === 'number' ? String(rawVal) : String(rawVal);
initial[cellKey] = { measurements: [strVal, '', ''] };
} else if (typeof rawVal === 'number') {
initial[cellKey] = { value: String(rawVal) };
} else if (typeof rawVal === 'string') {
initial[cellKey] = { value: rawVal };
}
}
});
if (Object.keys(initial).length > 0) setCellValues(initial);
// 부적합 내용 복원: 각 개소의 nonConformingContent를 수집
const remarks: string[] = [];
workItems.forEach((wi) => {
const itemData = inspectionDataMap.get(wi.id);
if (itemData?.nonConformingContent) {
remarks.push(itemData.nonConformingContent);
}
});
if (remarks.length > 0) setInadequateContent(remarks.join('\n'));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [inspectionDataMap, workItems]);
@@ -265,7 +318,11 @@ export const TemplateInspectionContent = forwardRef<InspectionContentRef, Templa
const sectionItem = columnItemMap.get(col.id);
if (!sectionItem) return acc;
const key = `${idx}-${col.id}`;
acc[`item_${sectionItem.id}`] = cellValues[key] || null;
const sectionId = itemSectionMap.get(sectionItem.id);
const saveKey = sectionId != null
? `section_${sectionId}_item_${sectionItem.id}`
: `item_${sectionItem.id}`;
acc[saveKey] = cellValues[key] || null;
return acc;
}, {} as Record<string, CellValue | null>),
}));
@@ -608,16 +665,16 @@ export const TemplateInspectionContent = forwardRef<InspectionContentRef, Templa
</>
)}
{/* ===== 푸터: 비고(좌) + 종합판정(우) 병렬 배치 ===== */}
<div className="mt-4 flex gap-4">
{/* ===== 푸터: 비고(좌) + 종합판정(우) 높이 동일 배치 ===== */}
<div className="mt-4 flex items-stretch gap-4">
<div className="flex-1">
<table className="w-full border-collapse text-xs">
<table className="w-full h-full border-collapse text-xs">
<tbody>
<tr>
<td className="border border-gray-400 px-3 py-2 bg-gray-100 font-medium" style={{ width: 100 }}>
<td className="border border-gray-400 px-3 py-2 bg-gray-100 font-medium align-top" style={{ width: 100 }}>
{template.footer_remark_label || '비고'}
</td>
<td className="border border-gray-400 px-3 py-2">
<td className="border border-gray-400 px-3 py-2 align-top">
<textarea
value={inadequateContent}
onChange={e => !readOnly && setInadequateContent(e.target.value)}
@@ -631,7 +688,7 @@ export const TemplateInspectionContent = forwardRef<InspectionContentRef, Templa
</table>
</div>
<div>
<table className="border-collapse text-xs">
<table className="h-full border-collapse text-xs">
<tbody>
<tr>
<td className="border border-gray-400 px-4 py-2 bg-gray-100 font-medium">
@@ -640,27 +697,14 @@ export const TemplateInspectionContent = forwardRef<InspectionContentRef, Templa
</tr>
<tr>
<td className="border border-gray-400 px-4 py-3 text-center">
{template.footer_judgement_options?.filter(o => o.trim()).length ? (
template.footer_judgement_options.filter(o => o.trim()).map((option, idx) => (
<span
key={idx}
className={`inline-block mx-1 px-2 py-0.5 border rounded ${
overallResult === option
? 'border-blue-500 bg-blue-50 text-blue-600 font-bold'
: overallResult && overallResult !== option
? 'border-gray-200 text-gray-300'
: 'border-gray-300 text-gray-500'
}`}
>
{option}
</span>
))
) : (
{overallResult ? (
<span className={`font-bold text-sm ${
overallResult === '합격' ? 'text-blue-600' : overallResult === '불합격' ? 'text-red-600' : 'text-gray-400'
overallResult === '합격' ? 'text-blue-600' : 'text-red-600'
}`}>
{overallResult || '-'}
{overallResult}
</span>
) : (
<span className="text-gray-300 text-sm">&nbsp;</span>
)}
</td>
</tr>