feat(WEB): 공정관리/작업지시/작업자화면 기능 강화 및 템플릿 개선
- 공정관리: ProcessDetail/ProcessForm/ProcessList 개선, StepDetail/StepForm 신규 추가 - 작업지시: WorkOrderDetail/Edit/List UI 개선, 작업지시서 문서 추가 - 작업자화면: WorkerScreen 대폭 개선, MaterialInputModal/WorkLogModal 수정, WorkItemCard 신규 - 영업주문: 주문 상세 페이지 개선 - 입고관리: 상세/actions 수정 - 템플릿: IntegratedDetailTemplate/IntegratedListTemplateV2/UniversalListPage 기능 확장 - UI: confirm-dialog 개선 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,481 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 절곡 중간검사 성적서 문서 콘텐츠
|
||||
*
|
||||
* 기획서 기준:
|
||||
* - 헤더: "중간검사성적서 (절곡)" + 결재란
|
||||
* - 기본정보: 제품명/슬랫, 규격/절곡, 수주처, 현장명 | 제품LOT NO, 로트크기, 검사일자, 검사자
|
||||
* + 제품명/KWE01, 마감유형/소니자감
|
||||
* - ■ 중간검사 기준서: 2섹션 (가이드레일류 + 연기차단재)
|
||||
* - ■ 중간검사 DATA: 분류, 제품명, 타입, 절곡상태결모양(양호/불량),
|
||||
* 길이(도면치수/측정값입력), 너비(도면치수/측정값입력),
|
||||
* 간격(포인트/도면치수/측정값입력), 판정(자동)
|
||||
* - 부적합 내용 / 종합판정(자동)
|
||||
*/
|
||||
|
||||
import { useState, useCallback, useMemo } from 'react';
|
||||
import type { WorkOrder } from '../types';
|
||||
|
||||
interface BendingInspectionContentProps {
|
||||
data: WorkOrder;
|
||||
readOnly?: boolean;
|
||||
}
|
||||
|
||||
type CheckStatus = '양호' | '불량' | null;
|
||||
|
||||
interface GapPoint {
|
||||
point: string; // ①②③④⑤
|
||||
designValue: string; // 도면치수
|
||||
measured: string; // 측정값 (입력)
|
||||
}
|
||||
|
||||
interface ProductRow {
|
||||
id: string;
|
||||
category: string;
|
||||
productName: string;
|
||||
productType: string;
|
||||
bendingStatus: CheckStatus;
|
||||
lengthDesign: string;
|
||||
lengthMeasured: string;
|
||||
widthDesign: string;
|
||||
widthMeasured: string;
|
||||
gapPoints: GapPoint[];
|
||||
}
|
||||
|
||||
const INITIAL_PRODUCTS: Omit<ProductRow, 'bendingStatus' | 'lengthMeasured' | 'widthMeasured'>[] = [
|
||||
{
|
||||
id: 'guide-rail', 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: '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: '' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export function BendingInspectionContent({ data: order, readOnly = false }: BendingInspectionContentProps) {
|
||||
const fullDate = new Date().toLocaleDateString('ko-KR', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
});
|
||||
|
||||
const today = new Date().toLocaleDateString('ko-KR', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
}).replace(/\. /g, '-').replace('.', '');
|
||||
|
||||
const documentNo = order.workOrderNo || 'ABC123';
|
||||
const primaryAssignee = order.assignees?.find(a => a.isPrimary)?.name || order.assignee || '-';
|
||||
|
||||
const [products, setProducts] = useState<ProductRow[]>(() =>
|
||||
INITIAL_PRODUCTS.map(p => ({
|
||||
...p,
|
||||
bendingStatus: null,
|
||||
lengthMeasured: '',
|
||||
widthMeasured: '',
|
||||
gapPoints: p.gapPoints.map(gp => ({ ...gp })),
|
||||
}))
|
||||
);
|
||||
|
||||
const [inadequateContent, setInadequateContent] = useState('');
|
||||
|
||||
const handleStatusChange = useCallback((productId: string, value: CheckStatus) => {
|
||||
if (readOnly) return;
|
||||
setProducts(prev => prev.map(p =>
|
||||
p.id === productId ? { ...p, bendingStatus: value } : p
|
||||
));
|
||||
}, [readOnly]);
|
||||
|
||||
const handleInputChange = useCallback((productId: string, field: 'lengthMeasured' | 'widthMeasured', value: string) => {
|
||||
if (readOnly) return;
|
||||
setProducts(prev => prev.map(p =>
|
||||
p.id === productId ? { ...p, [field]: value } : p
|
||||
));
|
||||
}, [readOnly]);
|
||||
|
||||
const handleGapMeasuredChange = useCallback((productId: string, gapIndex: number, value: string) => {
|
||||
if (readOnly) return;
|
||||
setProducts(prev => prev.map(p => {
|
||||
if (p.id !== productId) return p;
|
||||
const newGapPoints = p.gapPoints.map((gp, i) =>
|
||||
i === gapIndex ? { ...gp, measured: value } : gp
|
||||
);
|
||||
return { ...p, gapPoints: newGapPoints };
|
||||
}));
|
||||
}, [readOnly]);
|
||||
|
||||
// 행별 판정 자동 계산
|
||||
const getProductJudgment = useCallback((product: ProductRow): '적' | '부' | null => {
|
||||
if (product.bendingStatus === '불량') return '부';
|
||||
if (product.bendingStatus === '양호') return '적';
|
||||
return null;
|
||||
}, []);
|
||||
|
||||
// 종합판정 자동 계산
|
||||
const overallResult = useMemo(() => {
|
||||
const judgments = products.map(getProductJudgment);
|
||||
if (judgments.some(j => j === '부')) return '불합격';
|
||||
if (judgments.every(j => j === '적')) return '합격';
|
||||
return null;
|
||||
}, [products, getProductJudgment]);
|
||||
|
||||
const inputClass = 'w-full text-center border-0 bg-transparent focus:outline-none focus:ring-1 focus:ring-blue-500 rounded text-xs';
|
||||
|
||||
// 전체 행 수 계산 (간격 포인트 수 합계)
|
||||
const totalRows = products.reduce((sum, p) => sum + p.gapPoints.length, 0);
|
||||
|
||||
return (
|
||||
<div className="p-6 bg-white">
|
||||
{/* ===== 헤더 영역 ===== */}
|
||||
<div className="flex justify-between items-start mb-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">중간검사성적서 (절곡)</h1>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
문서번호: {documentNo} | 작성일자: {fullDate}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 결재란 */}
|
||||
<table className="border-collapse text-sm flex-shrink-0">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="border border-gray-400 px-4 py-1 text-center align-middle" rowSpan={3}>결<br/>재</td>
|
||||
<td className="border border-gray-400 px-6 py-1 text-center">작성</td>
|
||||
<td className="border border-gray-400 px-6 py-1 text-center">승인</td>
|
||||
<td className="border border-gray-400 px-6 py-1 text-center">승인</td>
|
||||
<td className="border border-gray-400 px-6 py-1 text-center">승인</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border border-gray-400 px-6 py-3 text-center">{primaryAssignee}</td>
|
||||
<td className="border border-gray-400 px-6 py-3 text-center text-gray-400">이름</td>
|
||||
<td className="border border-gray-400 px-6 py-3 text-center text-gray-400">이름</td>
|
||||
<td className="border border-gray-400 px-6 py-3 text-center text-gray-400">이름</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border border-gray-400 px-6 py-1 text-center">부서명</td>
|
||||
<td className="border border-gray-400 px-6 py-1 text-center">부서명</td>
|
||||
<td className="border border-gray-400 px-6 py-1 text-center">부서명</td>
|
||||
<td className="border border-gray-400 px-6 py-1 text-center">부서명</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* ===== 기본 정보 ===== */}
|
||||
<table className="w-full border-collapse text-xs mb-6">
|
||||
<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 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>
|
||||
<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">절곡</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?.length || 0} 개소</td>
|
||||
</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">{order.client || '-'}</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">{today}</td>
|
||||
</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">{order.projectName || '-'}</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">{primaryAssignee}</td>
|
||||
</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 bg-gray-50 px-3 py-2 font-medium">마감유형</td>
|
||||
<td className="border border-gray-400 px-3 py-2">소니자감</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{/* ===== 중간검사 기준서 (1) 가이드레일/케이스/하단마감재/하단L-BAR ===== */}
|
||||
<div className="mb-1 font-bold text-sm">■ 중간검사 기준서</div>
|
||||
<table className="w-full table-fixed border-collapse text-xs mb-4">
|
||||
<colgroup>
|
||||
<col style={{width: '80px'}} />
|
||||
<col style={{width: '180px'}} />
|
||||
<col style={{width: '52px'}} />
|
||||
<col style={{width: '52px'}} />
|
||||
<col />
|
||||
<col style={{width: '68px'}} />
|
||||
<col style={{width: '78px'}} />
|
||||
<col style={{width: '110px'}} />
|
||||
</colgroup>
|
||||
<tbody>
|
||||
{/* 헤더 */}
|
||||
<tr>
|
||||
<td className="border border-gray-400 px-2 py-1 font-bold text-center align-middle" rowSpan={4}>
|
||||
가이드레일<br/>케이스<br/>하단마감재<br/>하단 L-BAR
|
||||
</td>
|
||||
<th className="border border-gray-400 bg-gray-100 px-2 py-1 text-center">도해</th>
|
||||
<th className="border border-gray-400 bg-gray-100 px-2 py-1 text-center" colSpan={2}>검사항목</th>
|
||||
<th className="border border-gray-400 bg-gray-100 px-2 py-1 text-center">검사기준</th>
|
||||
<th className="border border-gray-400 bg-gray-100 px-2 py-1 text-center">검사방법</th>
|
||||
<th className="border border-gray-400 bg-gray-100 px-2 py-1 text-center">검사주기</th>
|
||||
<th className="border border-gray-400 bg-gray-100 px-2 py-1 text-center">관련규정</th>
|
||||
</tr>
|
||||
{/* 겉모양 | 절곡상태 */}
|
||||
<tr>
|
||||
<td className="border border-gray-400 p-2 text-center text-gray-500 align-middle text-xs" rowSpan={3}>
|
||||
절곡류 중간검사<br/>상세도면 참조
|
||||
</td>
|
||||
<td className="border border-gray-400 px-2 py-1 font-medium">겉모양</td>
|
||||
<td className="border border-gray-400 px-2 py-1 font-medium">절곡상태</td>
|
||||
<td className="border border-gray-400 px-2 py-1">사용상 해로운 결함이 없을것</td>
|
||||
<td className="border border-gray-400 px-2 py-1 text-center">육안검사</td>
|
||||
<td className="border border-gray-400 px-2 py-1 text-center" rowSpan={3}>n = 1, c = 0</td>
|
||||
<td className="border border-gray-400 px-2 py-1">KS F 4510 5.1항</td>
|
||||
</tr>
|
||||
{/* 치수 > 길이 */}
|
||||
<tr>
|
||||
<td className="border border-gray-400 px-2 py-1 font-medium bg-gray-50" rowSpan={2}>치수<br/>(mm)</td>
|
||||
<td className="border border-gray-400 px-2 py-1 font-medium">길이</td>
|
||||
<td className="border border-gray-400 px-2 py-1">도면치수 ± 4</td>
|
||||
<td className="border border-gray-400 px-2 py-1 text-center" rowSpan={2}>체크검사</td>
|
||||
<td className="border border-gray-400 px-2 py-1">KS F 4510 7항<br/>표9</td>
|
||||
</tr>
|
||||
{/* 치수 > 간격 */}
|
||||
<tr>
|
||||
<td className="border border-gray-400 px-2 py-1 font-medium">간격</td>
|
||||
<td className="border border-gray-400 px-2 py-1">도면치수 ± 2</td>
|
||||
<td className="border border-gray-400 px-2 py-1">KS F 4510 7항<br/>표9 / 자체규정</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{/* ===== 중간검사 기준서 (2) 연기차단재 ===== */}
|
||||
<table className="w-full table-fixed border-collapse text-xs mb-6">
|
||||
<colgroup>
|
||||
<col style={{width: '80px'}} />
|
||||
<col style={{width: '180px'}} />
|
||||
<col style={{width: '52px'}} />
|
||||
<col style={{width: '52px'}} />
|
||||
<col />
|
||||
<col style={{width: '68px'}} />
|
||||
<col style={{width: '78px'}} />
|
||||
<col style={{width: '110px'}} />
|
||||
</colgroup>
|
||||
<tbody>
|
||||
{/* 헤더 */}
|
||||
<tr>
|
||||
<td className="border border-gray-400 px-2 py-1 font-bold text-center align-middle" rowSpan={6}>
|
||||
연기차단재
|
||||
</td>
|
||||
<th className="border border-gray-400 bg-gray-100 px-2 py-1 text-center">도해</th>
|
||||
<th className="border border-gray-400 bg-gray-100 px-2 py-1 text-center" colSpan={2}>검사항목</th>
|
||||
<th className="border border-gray-400 bg-gray-100 px-2 py-1 text-center">검사기준</th>
|
||||
<th className="border border-gray-400 bg-gray-100 px-2 py-1 text-center">검사방법</th>
|
||||
<th className="border border-gray-400 bg-gray-100 px-2 py-1 text-center">검사주기</th>
|
||||
<th className="border border-gray-400 bg-gray-100 px-2 py-1 text-center">관련규정</th>
|
||||
</tr>
|
||||
{/* 겉모양 | 절곡상태 (row 1) */}
|
||||
<tr>
|
||||
<td className="border border-gray-400 p-2 text-center text-gray-300 align-middle" rowSpan={5}>
|
||||
<div className="h-32 flex items-center justify-center">도해 이미지 영역</div>
|
||||
</td>
|
||||
<td className="border border-gray-400 px-2 py-1 font-medium" rowSpan={2}>겉모양</td>
|
||||
<td className="border border-gray-400 px-2 py-1 font-medium" rowSpan={2}>절곡상태</td>
|
||||
<td className="border border-gray-400 px-2 py-1" rowSpan={2}>절단이 프레임에서 빠지지 않을것</td>
|
||||
<td className="border border-gray-400 px-2 py-1 text-center" rowSpan={2}>육안검사</td>
|
||||
<td className="border border-gray-400 px-2 py-1 text-center" rowSpan={5}>n = 1, c = 0</td>
|
||||
<td className="border border-gray-400 px-2 py-1">KS F 4510 5.1항</td>
|
||||
</tr>
|
||||
{/* 겉모양 | 절곡상태 (row 2 - 관련규정 분리) */}
|
||||
<tr>
|
||||
<td className="border border-gray-400 px-2 py-1">KS F 4510 7항<br/>표9 인용</td>
|
||||
</tr>
|
||||
{/* 치수 > 길이 */}
|
||||
<tr>
|
||||
<td className="border border-gray-400 px-2 py-1 font-medium bg-gray-50" rowSpan={3}>치수<br/>(mm)</td>
|
||||
<td className="border border-gray-400 px-2 py-1 font-medium">길이</td>
|
||||
<td className="border border-gray-400 px-2 py-1">도면치수 ± 4</td>
|
||||
<td className="border border-gray-400 px-2 py-1 text-center" rowSpan={3}>체크검사</td>
|
||||
<td className="border border-gray-400 px-2 py-1 text-center" rowSpan={3}>자체규정</td>
|
||||
</tr>
|
||||
{/* 치수 > 나비 */}
|
||||
<tr>
|
||||
<td className="border border-gray-400 px-2 py-1 font-medium">나비</td>
|
||||
<td className="border border-gray-400 px-2 py-1">W50 : 50 ± 5<br/>W80 : 80 ± 5</td>
|
||||
</tr>
|
||||
{/* 치수 > 간격 */}
|
||||
<tr>
|
||||
<td className="border border-gray-400 px-2 py-1 font-medium">간격</td>
|
||||
<td className="border border-gray-400 px-2 py-1">도면치수 ± 2</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{/* ===== 중간검사 DATA ===== */}
|
||||
<div className="mb-1 font-bold text-sm">■ 중간검사 DATA</div>
|
||||
<table className="w-full border-collapse text-xs mb-4">
|
||||
<thead>
|
||||
<tr className="bg-gray-100">
|
||||
<th className="border border-gray-400 p-1 w-14" 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}>타입</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>
|
||||
<th className="border border-gray-400 p-1" colSpan={3}>간격 (mm)</th>
|
||||
<th className="border border-gray-400 p-1 w-14" rowSpan={2}>판정<br/>(적/부)</th>
|
||||
</tr>
|
||||
<tr className="bg-gray-100">
|
||||
<th className="border border-gray-400 p-1 w-16">도면치수</th>
|
||||
<th className="border border-gray-400 p-1 w-16">측정값</th>
|
||||
<th className="border border-gray-400 p-1 w-16">도면치수</th>
|
||||
<th className="border border-gray-400 p-1 w-16">측정값</th>
|
||||
<th className="border border-gray-400 p-1 w-10">포인트</th>
|
||||
<th className="border border-gray-400 p-1 w-14">도면치수</th>
|
||||
<th className="border border-gray-400 p-1 w-14">측정값</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{products.map((product) => {
|
||||
const judgment = getProductJudgment(product);
|
||||
const rowCount = product.gapPoints.length;
|
||||
|
||||
return product.gapPoints.map((gap, gapIdx) => (
|
||||
<tr key={`${product.id}-${gapIdx}`}>
|
||||
{/* 첫 번째 간격 행에만 rowSpan 적용 */}
|
||||
{gapIdx === 0 && (
|
||||
<>
|
||||
<td className="border border-gray-400 p-1 text-center font-medium bg-gray-50" rowSpan={rowCount}>{product.category}</td>
|
||||
<td className="border border-gray-400 p-1" rowSpan={rowCount}>{product.productName}</td>
|
||||
<td className="border border-gray-400 p-1 text-center whitespace-pre-line" rowSpan={rowCount}>{product.productType}</td>
|
||||
{/* 절곡상태 - 양호/불량 체크 */}
|
||||
<td className="border border-gray-400 p-1" rowSpan={rowCount}>
|
||||
<div className="flex flex-col items-center gap-0.5">
|
||||
<label className="flex items-center gap-0.5 cursor-pointer text-xs">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={product.bendingStatus === '양호'}
|
||||
onChange={() => handleStatusChange(product.id, product.bendingStatus === '양호' ? null : '양호')}
|
||||
disabled={readOnly}
|
||||
className="w-3 h-3"
|
||||
/>
|
||||
양호
|
||||
</label>
|
||||
<label className="flex items-center gap-0.5 cursor-pointer text-xs">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={product.bendingStatus === '불량'}
|
||||
onChange={() => handleStatusChange(product.id, product.bendingStatus === '불량' ? null : '불량')}
|
||||
disabled={readOnly}
|
||||
className="w-3 h-3"
|
||||
/>
|
||||
불량
|
||||
</label>
|
||||
</div>
|
||||
</td>
|
||||
{/* 길이 */}
|
||||
<td className="border border-gray-400 p-1 text-center" rowSpan={rowCount}>{product.lengthDesign}</td>
|
||||
<td className="border border-gray-400 p-1" rowSpan={rowCount}>
|
||||
<input type="text" value={product.lengthMeasured} onChange={(e) => handleInputChange(product.id, 'lengthMeasured', e.target.value)} disabled={readOnly} className={inputClass} placeholder="-" />
|
||||
</td>
|
||||
{/* 너비 */}
|
||||
<td className="border border-gray-400 p-1 text-center" rowSpan={rowCount}>{product.widthDesign || 'N/A'}</td>
|
||||
<td className="border border-gray-400 p-1" rowSpan={rowCount}>
|
||||
<input type="text" value={product.widthMeasured} onChange={(e) => handleInputChange(product.id, 'widthMeasured', e.target.value)} disabled={readOnly} className={inputClass} placeholder="-" />
|
||||
</td>
|
||||
</>
|
||||
)}
|
||||
{/* 간격 - 포인트별 개별 행 */}
|
||||
<td className="border border-gray-400 p-1 text-center">{gap.point}</td>
|
||||
<td className="border border-gray-400 p-1 text-center">{gap.designValue}</td>
|
||||
<td className="border border-gray-400 p-1">
|
||||
<input type="text" value={gap.measured} onChange={(e) => handleGapMeasuredChange(product.id, gapIdx, e.target.value)} disabled={readOnly} className={inputClass} placeholder="-" />
|
||||
</td>
|
||||
{/* 판정 - 자동 (첫 행에만) */}
|
||||
{gapIdx === 0 && (
|
||||
<td className={`border border-gray-400 p-1 text-center font-bold ${
|
||||
judgment === '적' ? 'text-blue-600' : judgment === '부' ? 'text-red-600' : 'text-gray-300'
|
||||
}`} rowSpan={rowCount}>
|
||||
{judgment || '-'}
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
));
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{/* ===== 부적합 내용 + 종합판정 ===== */}
|
||||
<table className="w-full border-collapse text-xs">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="border border-gray-400 bg-gray-100 px-3 py-2 font-medium w-24 align-top">부적합 내용</td>
|
||||
<td className="border border-gray-400 px-3 py-2" colSpan={2}>
|
||||
<textarea value={inadequateContent} onChange={(e) => !readOnly && setInadequateContent(e.target.value)} disabled={readOnly}
|
||||
className="w-full border-0 bg-transparent focus:outline-none focus:ring-1 focus:ring-blue-500 rounded text-xs resize-none" rows={2} />
|
||||
</td>
|
||||
<td className="border border-gray-400 bg-gray-100 px-3 py-2 font-medium text-center w-24">종합판정</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border border-gray-400 px-3 py-2" colSpan={3}></td>
|
||||
<td className={`border border-gray-400 px-3 py-2 text-center font-bold text-sm ${
|
||||
overallResult === '합격' ? 'text-blue-600' : overallResult === '불합격' ? 'text-red-600' : 'text-gray-400'
|
||||
}`}>
|
||||
{overallResult || '합격'}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 절곡 작업일지 문서 콘텐츠
|
||||
*
|
||||
* 기획서 기준 구성:
|
||||
* - 헤더: "작업일지 (절곡)" + 문서번호/작성일자 + 결재란(결재|작성/승인/승인/승인)
|
||||
* - 신청업체(수주일,수주처,담당자,연락처) / 신청내용(현장명,작업일자,제품LOT NO,생산담당자,출고예정일)
|
||||
* - 제품명 / 재질 / 마감 / 유형 테이블
|
||||
* - 작업내역: 유형명, 세부품명, 재질, 입고 & 생산 LOT NO, 길이/규격, 수량
|
||||
* - 생산량 합계 [kg]: SUS / EGI
|
||||
*/
|
||||
|
||||
import type { WorkOrder } from '../types';
|
||||
import { SectionHeader } from '@/components/document-system';
|
||||
|
||||
interface BendingWorkLogContentProps {
|
||||
data: WorkOrder;
|
||||
}
|
||||
|
||||
export function BendingWorkLogContent({ data: order }: BendingWorkLogContentProps) {
|
||||
const today = new Date().toLocaleDateString('ko-KR', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
}).replace(/\. /g, '-').replace('.', '');
|
||||
|
||||
const fullDate = new Date().toLocaleDateString('ko-KR', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
});
|
||||
|
||||
const documentNo = order.workOrderNo || 'ABC123';
|
||||
const primaryAssignee = order.assignees?.find(a => a.isPrimary)?.name || order.assignee || '-';
|
||||
const items = order.items || [];
|
||||
|
||||
const formattedDueDate = order.dueDate !== '-'
|
||||
? new Date(order.dueDate).toLocaleDateString('ko-KR', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
}).replace(/\. /g, '-').replace('.', '')
|
||||
: '-';
|
||||
|
||||
// 빈 행 수 (기획서에 여러 빈 행 표시)
|
||||
const EMPTY_ROWS = 4;
|
||||
|
||||
return (
|
||||
<div className="p-6 bg-white">
|
||||
{/* ===== 헤더 영역 ===== */}
|
||||
<div className="flex justify-between items-start mb-6">
|
||||
{/* 좌측: 제목 + 문서번호 */}
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">작업일지 (절곡)</h1>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
문서번호: {documentNo} | 작성일자: {fullDate}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 우측: 결재란 */}
|
||||
<table className="border-collapse text-sm flex-shrink-0">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="border border-gray-400 px-4 py-1 text-center align-middle" rowSpan={3}>결<br/>재</td>
|
||||
<td className="border border-gray-400 px-6 py-1 text-center">작성</td>
|
||||
<td className="border border-gray-400 px-6 py-1 text-center">승인</td>
|
||||
<td className="border border-gray-400 px-6 py-1 text-center">승인</td>
|
||||
<td className="border border-gray-400 px-6 py-1 text-center">승인</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border border-gray-400 px-6 py-3 text-center">{primaryAssignee}</td>
|
||||
<td className="border border-gray-400 px-6 py-3 text-center text-gray-400">이름</td>
|
||||
<td className="border border-gray-400 px-6 py-3 text-center text-gray-400">이름</td>
|
||||
<td className="border border-gray-400 px-6 py-3 text-center text-gray-400">이름</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border border-gray-400 px-6 py-1 text-center">부서명</td>
|
||||
<td className="border border-gray-400 px-6 py-1 text-center">부서명</td>
|
||||
<td className="border border-gray-400 px-6 py-1 text-center">부서명</td>
|
||||
<td className="border border-gray-400 px-6 py-1 text-center">부서명</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* ===== 신청업체 / 신청내용 ===== */}
|
||||
<table className="w-full border-collapse text-xs mb-6">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="border border-gray-400 bg-gray-100 px-3 py-2 text-center" colSpan={2}>신청업체</th>
|
||||
<th className="border border-gray-400 bg-gray-100 px-3 py-2 text-center" colSpan={2}>신청내용</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<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">{order.salesOrderDate || '-'}</td>
|
||||
<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">{order.projectName}</td>
|
||||
</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">{order.client}</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">{today}</td>
|
||||
</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">{primaryAssignee}</td>
|
||||
<td className="border border-gray-400 bg-gray-50 px-3 py-2 font-medium">제품 LOT NO</td>
|
||||
<td className="border border-gray-400 px-3 py-2">{order.lotNo}</td>
|
||||
</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">-</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">{primaryAssignee}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border border-gray-400 bg-gray-50 px-3 py-2 font-medium" colSpan={2}></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">{formattedDueDate}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{/* ===== 제품 정보 ===== */}
|
||||
<table className="w-full border-collapse text-xs mb-6">
|
||||
<thead>
|
||||
<tr className="bg-gray-100">
|
||||
<th className="border border-gray-400 p-2">제품명</th>
|
||||
<th className="border border-gray-400 p-2">재질</th>
|
||||
<th className="border border-gray-400 p-2">마감</th>
|
||||
<th className="border border-gray-400 p-2">유형</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="border border-gray-400 p-2"> </td>
|
||||
<td className="border border-gray-400 p-2"> </td>
|
||||
<td className="border border-gray-400 p-2"> </td>
|
||||
<td className="border border-gray-400 p-2"> </td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{/* ===== 작업내역 ===== */}
|
||||
<SectionHeader variant="dark">작업내역</SectionHeader>
|
||||
<table className="w-full border-collapse text-xs mb-6">
|
||||
<thead>
|
||||
<tr className="bg-gray-100">
|
||||
<th className="border border-gray-400 p-2">유형명</th>
|
||||
<th className="border border-gray-400 p-2">세부품명</th>
|
||||
<th className="border border-gray-400 p-2">재질</th>
|
||||
<th className="border border-gray-400 p-2">입고 & 생산 LOT NO</th>
|
||||
<th className="border border-gray-400 p-2">길이/규격</th>
|
||||
<th className="border border-gray-400 p-2 w-16">수량</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{items.length > 0 ? (
|
||||
items.map((item, idx) => (
|
||||
<tr key={item.id}>
|
||||
<td className="border border-gray-400 p-2">{item.productName}</td>
|
||||
<td className="border border-gray-400 p-2">-</td>
|
||||
<td className="border border-gray-400 p-2 text-center">-</td>
|
||||
<td className="border border-gray-400 p-2 text-center">{order.lotNo}</td>
|
||||
<td className="border border-gray-400 p-2 text-center">-</td>
|
||||
<td className="border border-gray-400 p-2 text-center">{item.quantity}</td>
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
Array.from({ length: EMPTY_ROWS }).map((_, idx) => (
|
||||
<tr key={idx}>
|
||||
<td className="border border-gray-400 p-2"> </td>
|
||||
<td className="border border-gray-400 p-2"> </td>
|
||||
<td className="border border-gray-400 p-2"> </td>
|
||||
<td className="border border-gray-400 p-2"> </td>
|
||||
<td className="border border-gray-400 p-2"> </td>
|
||||
<td className="border border-gray-400 p-2"> </td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{/* ===== 생산량 합계 [kg] ===== */}
|
||||
<table className="w-full border-collapse text-xs mb-6">
|
||||
<thead>
|
||||
<tr className="bg-gray-100">
|
||||
<th className="border border-gray-400 p-2">생산량 합계 [kg]</th>
|
||||
<th className="border border-gray-400 p-2">SUS</th>
|
||||
<th className="border border-gray-400 p-2">EGI</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="border border-gray-400 p-2"> </td>
|
||||
<td className="border border-gray-400 p-2"> </td>
|
||||
<td className="border border-gray-400 p-2"> </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border border-gray-400 p-2"> </td>
|
||||
<td className="border border-gray-400 p-2"> </td>
|
||||
<td className="border border-gray-400 p-2"> </td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 중간검사 성적서 모달
|
||||
*
|
||||
* DocumentViewer 껍데기 안에 공정별 검사 성적서 콘텐츠를 표시
|
||||
* - screen: ScreenInspectionContent
|
||||
* - slat: SlatInspectionContent
|
||||
* - bending: BendingInspectionContent
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import { DocumentViewer } from '@/components/document-system';
|
||||
import { getWorkOrderById } from '../actions';
|
||||
import type { WorkOrder, ProcessType } from '../types';
|
||||
import { ScreenInspectionContent } from './ScreenInspectionContent';
|
||||
import { SlatInspectionContent } from './SlatInspectionContent';
|
||||
import { BendingInspectionContent } from './BendingInspectionContent';
|
||||
|
||||
const PROCESS_LABELS: Record<ProcessType, string> = {
|
||||
screen: '스크린',
|
||||
slat: '슬랫',
|
||||
bending: '절곡',
|
||||
};
|
||||
|
||||
interface InspectionReportModalProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
workOrderId: string | null;
|
||||
processType?: ProcessType;
|
||||
}
|
||||
|
||||
export function InspectionReportModal({
|
||||
open,
|
||||
onOpenChange,
|
||||
workOrderId,
|
||||
processType = 'screen',
|
||||
}: InspectionReportModalProps) {
|
||||
const [order, setOrder] = useState<WorkOrder | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// 목업 WorkOrder 생성
|
||||
const createMockOrder = (id: string, pType: ProcessType): WorkOrder => ({
|
||||
id,
|
||||
workOrderNo: 'KD-WO-260129-01',
|
||||
lotNo: 'KD-SA-260129-01',
|
||||
processId: 1,
|
||||
processName: pType === 'slat' ? '슬랫' : pType === 'bending' ? '절곡' : '스크린',
|
||||
processCode: pType,
|
||||
processType: pType,
|
||||
status: 'in_progress',
|
||||
client: '(주)경동',
|
||||
projectName: '서울 강남 현장',
|
||||
dueDate: '2026-02-05',
|
||||
assignee: '홍길동',
|
||||
assignees: [{ id: '1', name: '홍길동', isPrimary: true }],
|
||||
orderDate: '2026-01-20',
|
||||
scheduledDate: '2026-01-29',
|
||||
shipmentDate: '2026-02-05',
|
||||
salesOrderDate: '2026-01-15',
|
||||
isAssigned: true,
|
||||
isStarted: true,
|
||||
priority: 3,
|
||||
priorityLabel: '긴급',
|
||||
shutterCount: 12,
|
||||
department: '생산부',
|
||||
items: [
|
||||
{ id: '1', no: 1, status: 'in_progress', productName: '와이어 스크린', floorCode: '1층/FSS-01', specification: '8,260 X 8,350', quantity: 2, unit: 'EA' },
|
||||
{ id: '2', no: 2, status: 'waiting', productName: '메쉬 스크린', floorCode: '2층/FSS-03', specification: '6,400 X 5,200', quantity: 4, unit: 'EA' },
|
||||
{ id: '3', no: 3, status: 'completed', productName: '광폭 와이어', floorCode: '3층/FSS-05', specification: '12,000 X 4,500', quantity: 1, unit: 'EA' },
|
||||
],
|
||||
currentStep: { key: 'cutting', label: '절단', order: 2 },
|
||||
completedSteps: ['material_input'],
|
||||
totalProgress: 25,
|
||||
issues: [],
|
||||
memo: '',
|
||||
createdAt: '2026-01-20T09:00:00',
|
||||
updatedAt: '2026-01-29T14:00:00',
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (open && workOrderId) {
|
||||
// 목업 ID인 경우 API 호출 생략
|
||||
if (workOrderId.startsWith('mock-')) {
|
||||
setOrder(createMockOrder(workOrderId, processType));
|
||||
setError(null);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
getWorkOrderById(workOrderId)
|
||||
.then((result) => {
|
||||
if (result.success && result.data) {
|
||||
setOrder(result.data);
|
||||
} else {
|
||||
setError(result.error || '데이터를 불러올 수 없습니다.');
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
setError('서버 오류가 발생했습니다.');
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false);
|
||||
});
|
||||
} else if (!open) {
|
||||
setOrder(null);
|
||||
setError(null);
|
||||
}
|
||||
}, [open, workOrderId, processType]);
|
||||
|
||||
if (!workOrderId) return null;
|
||||
|
||||
const processLabel = PROCESS_LABELS[processType] || '스크린';
|
||||
const subtitle = order ? `${processLabel} 생산부서` : undefined;
|
||||
|
||||
const renderContent = () => {
|
||||
if (!order) return null;
|
||||
|
||||
switch (processType) {
|
||||
case 'screen':
|
||||
return <ScreenInspectionContent data={order} />;
|
||||
case 'slat':
|
||||
return <SlatInspectionContent data={order} />;
|
||||
case 'bending':
|
||||
return <BendingInspectionContent data={order} />;
|
||||
default:
|
||||
return <ScreenInspectionContent data={order} />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<DocumentViewer
|
||||
title="중간검사 성적서"
|
||||
subtitle={subtitle}
|
||||
preset="inspection"
|
||||
open={open}
|
||||
onOpenChange={onOpenChange}
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center h-64 bg-white">
|
||||
<Loader2 className="w-8 h-8 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
) : error || !order ? (
|
||||
<div className="flex flex-col items-center justify-center h-64 gap-4 bg-white">
|
||||
<p className="text-muted-foreground">{error || '데이터를 불러올 수 없습니다.'}</p>
|
||||
</div>
|
||||
) : (
|
||||
renderContent()
|
||||
)}
|
||||
</DocumentViewer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,383 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 스크린 중간검사 성적서 문서 콘텐츠
|
||||
*
|
||||
* 기획서 기준:
|
||||
* - 헤더: "중간검사성적서 (스크린)" + 결재란
|
||||
* - 기본정보: 제품명/스크린, 규격/와이어 글라스 코팅직물, 수주처, 현장명 | 제품LOT NO, 로트크기, 검사일자, 검사자
|
||||
* - ■ 중간검사 기준서: 도해 + 검사항목/검사기준/검사방법/검사주기/관련규정
|
||||
* 가공상태, 재봉상태, 조립상태, 치수(길이/높이/간격)
|
||||
* - ■ 중간검사 DATA: No, 가공상태결모양(양호/불량), 재봉상태결모양(양호/불량), 조립상태(양호/불량),
|
||||
* 길이(도면치수/측정값입력), 나비(도면치수/측정값입력), 간격(기준치/OK·NG선택), 판정(자동)
|
||||
* - 부적합 내용 / 종합판정(자동)
|
||||
*/
|
||||
|
||||
import { useState, useCallback, useMemo } from 'react';
|
||||
import type { WorkOrder } from '../types';
|
||||
|
||||
interface ScreenInspectionContentProps {
|
||||
data: WorkOrder;
|
||||
readOnly?: boolean;
|
||||
}
|
||||
|
||||
type CheckStatus = '양호' | '불량' | null;
|
||||
type GapResult = 'OK' | 'NG' | null;
|
||||
|
||||
interface InspectionRow {
|
||||
id: number;
|
||||
processStatus: CheckStatus; // 가공상태 결모양
|
||||
sewingStatus: CheckStatus; // 재봉상태 결모양
|
||||
assemblyStatus: CheckStatus; // 조립상태
|
||||
lengthDesign: string; // 길이 도면치수 (표시용)
|
||||
lengthMeasured: string; // 길이 측정값 (입력)
|
||||
widthDesign: string; // 나비 도면치수 (표시용)
|
||||
widthMeasured: string; // 나비 측정값 (입력)
|
||||
gapStandard: string; // 간격 기준치 (표시용)
|
||||
gapResult: GapResult; // 간격 측정값 (OK/NG 선택)
|
||||
}
|
||||
|
||||
const DEFAULT_ROW_COUNT = 6;
|
||||
|
||||
export function ScreenInspectionContent({ data: order, readOnly = false }: ScreenInspectionContentProps) {
|
||||
const fullDate = new Date().toLocaleDateString('ko-KR', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
});
|
||||
|
||||
const today = new Date().toLocaleDateString('ko-KR', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
}).replace(/\. /g, '-').replace('.', '');
|
||||
|
||||
const documentNo = order.workOrderNo || 'ABC123';
|
||||
const primaryAssignee = order.assignees?.find(a => a.isPrimary)?.name || order.assignee || '-';
|
||||
|
||||
const [rows, setRows] = useState<InspectionRow[]>(() =>
|
||||
Array.from({ length: DEFAULT_ROW_COUNT }, (_, i) => ({
|
||||
id: i + 1,
|
||||
processStatus: null,
|
||||
sewingStatus: null,
|
||||
assemblyStatus: null,
|
||||
lengthDesign: '7,400',
|
||||
lengthMeasured: '',
|
||||
widthDesign: '2,950',
|
||||
widthMeasured: '',
|
||||
gapStandard: '400 이하',
|
||||
gapResult: null,
|
||||
}))
|
||||
);
|
||||
|
||||
const [inadequateContent, setInadequateContent] = useState('');
|
||||
|
||||
const handleStatusChange = useCallback((rowId: number, field: 'processStatus' | 'sewingStatus' | 'assemblyStatus', value: CheckStatus) => {
|
||||
if (readOnly) return;
|
||||
setRows(prev => prev.map(row =>
|
||||
row.id === rowId ? { ...row, [field]: value } : row
|
||||
));
|
||||
}, [readOnly]);
|
||||
|
||||
const handleInputChange = useCallback((rowId: number, field: 'lengthMeasured' | 'widthMeasured', value: string) => {
|
||||
if (readOnly) return;
|
||||
setRows(prev => prev.map(row =>
|
||||
row.id === rowId ? { ...row, [field]: value } : row
|
||||
));
|
||||
}, [readOnly]);
|
||||
|
||||
const handleGapChange = useCallback((rowId: number, value: GapResult) => {
|
||||
if (readOnly) return;
|
||||
setRows(prev => prev.map(row =>
|
||||
row.id === rowId ? { ...row, gapResult: value } : row
|
||||
));
|
||||
}, [readOnly]);
|
||||
|
||||
// 행별 판정 자동 계산
|
||||
const getRowJudgment = useCallback((row: InspectionRow): '적' | '부' | null => {
|
||||
const { processStatus, sewingStatus, assemblyStatus, gapResult } = row;
|
||||
// 하나라도 불량 or NG → 부
|
||||
if (processStatus === '불량' || sewingStatus === '불량' || assemblyStatus === '불량' || gapResult === 'NG') {
|
||||
return '부';
|
||||
}
|
||||
// 모두 양호 + OK → 적
|
||||
if (processStatus === '양호' && sewingStatus === '양호' && assemblyStatus === '양호' && gapResult === 'OK') {
|
||||
return '적';
|
||||
}
|
||||
return null;
|
||||
}, []);
|
||||
|
||||
// 종합판정 자동 계산
|
||||
const overallResult = useMemo(() => {
|
||||
const judgments = rows.map(getRowJudgment);
|
||||
if (judgments.some(j => j === '부')) return '불합격';
|
||||
if (judgments.every(j => j === '적')) return '합격';
|
||||
return null;
|
||||
}, [rows, getRowJudgment]);
|
||||
|
||||
// 체크박스 렌더 (양호/불량)
|
||||
const renderCheckStatus = (rowId: number, field: 'processStatus' | 'sewingStatus' | 'assemblyStatus', value: CheckStatus) => (
|
||||
<td className="border border-gray-400 p-1">
|
||||
<div className="flex flex-col items-center gap-0.5">
|
||||
<label className="flex items-center gap-0.5 cursor-pointer text-xs">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={value === '양호'}
|
||||
onChange={() => handleStatusChange(rowId, field, value === '양호' ? null : '양호')}
|
||||
disabled={readOnly}
|
||||
className="w-3 h-3"
|
||||
/>
|
||||
양호
|
||||
</label>
|
||||
<label className="flex items-center gap-0.5 cursor-pointer text-xs">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={value === '불량'}
|
||||
onChange={() => handleStatusChange(rowId, field, value === '불량' ? null : '불량')}
|
||||
disabled={readOnly}
|
||||
className="w-3 h-3"
|
||||
/>
|
||||
불량
|
||||
</label>
|
||||
</div>
|
||||
</td>
|
||||
);
|
||||
|
||||
const inputClass = 'w-full text-center border-0 bg-transparent focus:outline-none focus:ring-1 focus:ring-blue-500 rounded text-xs';
|
||||
|
||||
return (
|
||||
<div className="p-6 bg-white">
|
||||
{/* ===== 헤더 영역 ===== */}
|
||||
<div className="flex justify-between items-start mb-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">중간검사성적서 (스크린)</h1>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
문서번호: {documentNo} | 작성일자: {fullDate}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 결재란 */}
|
||||
<table className="border-collapse text-sm flex-shrink-0">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="border border-gray-400 px-4 py-1 text-center align-middle" rowSpan={3}>결<br/>재</td>
|
||||
<td className="border border-gray-400 px-6 py-1 text-center">작성</td>
|
||||
<td className="border border-gray-400 px-6 py-1 text-center">승인</td>
|
||||
<td className="border border-gray-400 px-6 py-1 text-center">승인</td>
|
||||
<td className="border border-gray-400 px-6 py-1 text-center">승인</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border border-gray-400 px-6 py-3 text-center">{primaryAssignee}</td>
|
||||
<td className="border border-gray-400 px-6 py-3 text-center text-gray-400">이름</td>
|
||||
<td className="border border-gray-400 px-6 py-3 text-center text-gray-400">이름</td>
|
||||
<td className="border border-gray-400 px-6 py-3 text-center text-gray-400">이름</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border border-gray-400 px-6 py-1 text-center">부서명</td>
|
||||
<td className="border border-gray-400 px-6 py-1 text-center">부서명</td>
|
||||
<td className="border border-gray-400 px-6 py-1 text-center">부서명</td>
|
||||
<td className="border border-gray-400 px-6 py-1 text-center">부서명</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* ===== 기본 정보 ===== */}
|
||||
<table className="w-full border-collapse text-xs mb-6">
|
||||
<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 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>
|
||||
<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">와이어 글라스 코팅직물</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?.length || 0} 개소</td>
|
||||
</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">{order.client || '-'}</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">{today}</td>
|
||||
</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">{order.projectName || '-'}</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">{primaryAssignee}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{/* ===== 중간검사 기준서 ===== */}
|
||||
<div className="mb-1 font-bold text-sm">■ 중간검사 기준서</div>
|
||||
<table className="w-full border-collapse text-xs mb-6">
|
||||
<tbody>
|
||||
<tr>
|
||||
{/* 도해 영역 */}
|
||||
<td className="border border-gray-400 p-4 text-center text-gray-300 align-middle w-1/4" rowSpan={8}>
|
||||
<div className="h-40 flex items-center justify-center">도해 이미지 영역</div>
|
||||
</td>
|
||||
{/* 헤더 행 */}
|
||||
<th className="border border-gray-400 bg-gray-100 px-2 py-1 text-center" colSpan={2}>검사항목</th>
|
||||
<th className="border border-gray-400 bg-gray-100 px-2 py-1 text-center">검사기준</th>
|
||||
<th className="border border-gray-400 bg-gray-100 px-2 py-1 text-center">검사방법</th>
|
||||
<th className="border border-gray-400 bg-gray-100 px-2 py-1 text-center">검사주기</th>
|
||||
<th className="border border-gray-400 bg-gray-100 px-2 py-1 text-center">관련규정</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border border-gray-400 px-2 py-1 font-medium" colSpan={2}>가공상태</td>
|
||||
<td className="border border-gray-400 px-2 py-1">사용상 해로운 결함이 없을것</td>
|
||||
<td className="border border-gray-400 px-2 py-1 text-center">육안검사</td>
|
||||
<td className="border border-gray-400 px-2 py-1 text-center"></td>
|
||||
<td className="border border-gray-400 px-2 py-1">KS F 4510 5.1항</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border border-gray-400 px-2 py-1 font-medium" colSpan={2}>재봉상태</td>
|
||||
<td className="border border-gray-400 px-2 py-1">내화실험 되어 견고하게</td>
|
||||
<td className="border border-gray-400 px-2 py-1 text-center"></td>
|
||||
<td className="border border-gray-400 px-2 py-1 text-center"></td>
|
||||
<td className="border border-gray-400 px-2 py-1"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border border-gray-400 px-2 py-1 font-medium" colSpan={2}>조립상태</td>
|
||||
<td className="border border-gray-400 px-2 py-1">엔도확인 견고하게 조립되어야 함</td>
|
||||
<td className="border border-gray-400 px-2 py-1 text-center"></td>
|
||||
<td className="border border-gray-400 px-2 py-1 text-center"></td>
|
||||
<td className="border border-gray-400 px-2 py-1">KS F 4510<br/>n = 1, c = 0</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border border-gray-400 px-2 py-1 font-medium bg-gray-50" rowSpan={3}>치수<br/>(mm)</td>
|
||||
<td className="border border-gray-400 px-2 py-1 font-medium">길이</td>
|
||||
<td className="border border-gray-400 px-2 py-1">도면치수 ± 4</td>
|
||||
<td className="border border-gray-400 px-2 py-1 text-center"></td>
|
||||
<td className="border border-gray-400 px-2 py-1 text-center"></td>
|
||||
<td className="border border-gray-400 px-2 py-1"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border border-gray-400 px-2 py-1 font-medium">높이</td>
|
||||
<td className="border border-gray-400 px-2 py-1">도면치수 + 제한없음 − 40</td>
|
||||
<td className="border border-gray-400 px-2 py-1 text-center"></td>
|
||||
<td className="border border-gray-400 px-2 py-1 text-center"></td>
|
||||
<td className="border border-gray-400 px-2 py-1"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border border-gray-400 px-2 py-1 font-medium">간격</td>
|
||||
<td className="border border-gray-400 px-2 py-1">400 이하</td>
|
||||
<td className="border border-gray-400 px-2 py-1 text-center">GONO 게이지</td>
|
||||
<td className="border border-gray-400 px-2 py-1 text-center"></td>
|
||||
<td className="border border-gray-400 px-2 py-1">자체규정</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{/* ===== 중간검사 DATA ===== */}
|
||||
<div className="mb-1 font-bold text-sm">■ 중간검사 DATA</div>
|
||||
<table className="w-full border-collapse text-xs mb-4">
|
||||
<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-16" rowSpan={2}>가공상태<br/>결모양</th>
|
||||
<th className="border border-gray-400 p-1 w-16" rowSpan={2}>재봉상태<br/>결모양</th>
|
||||
<th className="border border-gray-400 p-1 w-16" rowSpan={2}>조립상태</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>
|
||||
<th className="border border-gray-400 p-1" colSpan={2}>간격 (mm)</th>
|
||||
<th className="border border-gray-400 p-1 w-14" rowSpan={2}>판정<br/>(적/부)</th>
|
||||
</tr>
|
||||
<tr className="bg-gray-100">
|
||||
<th className="border border-gray-400 p-1 w-16">도면치수</th>
|
||||
<th className="border border-gray-400 p-1 w-16">측정값</th>
|
||||
<th className="border border-gray-400 p-1 w-16">도면치수</th>
|
||||
<th className="border border-gray-400 p-1 w-16">측정값</th>
|
||||
<th className="border border-gray-400 p-1 w-16">기준치</th>
|
||||
<th className="border border-gray-400 p-1 w-16">측정값</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.map((row) => {
|
||||
const judgment = getRowJudgment(row);
|
||||
return (
|
||||
<tr key={row.id}>
|
||||
<td className="border border-gray-400 p-1 text-center">{row.id}</td>
|
||||
{/* 가공상태 - 양호/불량 체크 */}
|
||||
{renderCheckStatus(row.id, 'processStatus', row.processStatus)}
|
||||
{/* 재봉상태 - 양호/불량 체크 */}
|
||||
{renderCheckStatus(row.id, 'sewingStatus', row.sewingStatus)}
|
||||
{/* 조립상태 - 양호/불량 체크 */}
|
||||
{renderCheckStatus(row.id, 'assemblyStatus', row.assemblyStatus)}
|
||||
{/* 길이 - 도면치수 표시 + 측정값 입력 */}
|
||||
<td className="border border-gray-400 p-1 text-center">{row.lengthDesign}</td>
|
||||
<td className="border border-gray-400 p-1">
|
||||
<input type="text" value={row.lengthMeasured} onChange={(e) => handleInputChange(row.id, 'lengthMeasured', e.target.value)} disabled={readOnly} className={inputClass} placeholder="-" />
|
||||
</td>
|
||||
{/* 나비 - 도면치수 표시 + 측정값 입력 */}
|
||||
<td className="border border-gray-400 p-1 text-center">{row.widthDesign}</td>
|
||||
<td className="border border-gray-400 p-1">
|
||||
<input type="text" value={row.widthMeasured} onChange={(e) => handleInputChange(row.id, 'widthMeasured', e.target.value)} disabled={readOnly} className={inputClass} placeholder="-" />
|
||||
</td>
|
||||
{/* 간격 - 기준치 표시 + OK/NG 선택 */}
|
||||
<td className="border border-gray-400 p-1 text-center">{row.gapStandard}</td>
|
||||
<td className="border border-gray-400 p-1">
|
||||
<div className="flex flex-col items-center gap-0.5">
|
||||
<label className="flex items-center gap-0.5 cursor-pointer text-xs">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={row.gapResult === 'OK'}
|
||||
onChange={() => handleGapChange(row.id, row.gapResult === 'OK' ? null : 'OK')}
|
||||
disabled={readOnly}
|
||||
className="w-3 h-3"
|
||||
/>
|
||||
OK
|
||||
</label>
|
||||
<label className="flex items-center gap-0.5 cursor-pointer text-xs">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={row.gapResult === 'NG'}
|
||||
onChange={() => handleGapChange(row.id, row.gapResult === 'NG' ? null : 'NG')}
|
||||
disabled={readOnly}
|
||||
className="w-3 h-3"
|
||||
/>
|
||||
NG
|
||||
</label>
|
||||
</div>
|
||||
</td>
|
||||
{/* 판정 - 자동 계산 */}
|
||||
<td className={`border border-gray-400 p-1 text-center font-bold ${
|
||||
judgment === '적' ? 'text-blue-600' : judgment === '부' ? 'text-red-600' : 'text-gray-300'
|
||||
}`}>
|
||||
{judgment || '-'}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{/* ===== 부적합 내용 + 종합판정 ===== */}
|
||||
<table className="w-full border-collapse text-xs">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="border border-gray-400 bg-gray-100 px-3 py-2 font-medium w-24 align-top">부적합 내용</td>
|
||||
<td className="border border-gray-400 px-3 py-2" colSpan={2}>
|
||||
<textarea value={inadequateContent} onChange={(e) => !readOnly && setInadequateContent(e.target.value)} disabled={readOnly}
|
||||
className="w-full border-0 bg-transparent focus:outline-none focus:ring-1 focus:ring-blue-500 rounded text-xs resize-none" rows={2} />
|
||||
</td>
|
||||
<td className="border border-gray-400 bg-gray-100 px-3 py-2 font-medium text-center w-24">종합판정</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border border-gray-400 px-3 py-2" colSpan={3}></td>
|
||||
<td className={`border border-gray-400 px-3 py-2 text-center font-bold text-sm ${
|
||||
overallResult === '합격' ? 'text-blue-600' : overallResult === '불합격' ? 'text-red-600' : 'text-gray-400'
|
||||
}`}>
|
||||
{overallResult || '합격'}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 스크린 작업일지 문서 콘텐츠
|
||||
*
|
||||
* 기획서 스크린샷 기준 구성:
|
||||
* - 헤더: "작업일지 (스크린)" + 문서번호/작성일자 + 결재란(작성/승인/승인/승인)
|
||||
* - 신청업체(수주일,수주처,담당자,연락처) / 신청내용(현장명,작업일자,제품LOT NO,생산담당자,출고예정일)
|
||||
* - 작업내역 테이블: No, 입고 LOT NO, 제품명, 부호, 제작사이즈(가로/세로), 나머지 높이,
|
||||
* 규격(매수)(1220/900/600/400/300), 제작, 재단 사항, 잔량, 완료
|
||||
* - 합계
|
||||
* - 내화실 입고 LOT NO
|
||||
* - 비고
|
||||
*/
|
||||
|
||||
import type { WorkOrder } from '../types';
|
||||
import { SectionHeader } from '@/components/document-system';
|
||||
|
||||
interface ScreenWorkLogContentProps {
|
||||
data: WorkOrder;
|
||||
}
|
||||
|
||||
export function ScreenWorkLogContent({ data: order }: ScreenWorkLogContentProps) {
|
||||
const today = new Date().toLocaleDateString('ko-KR', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
}).replace(/\. /g, '-').replace('.', '');
|
||||
|
||||
const fullDate = new Date().toLocaleDateString('ko-KR', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
});
|
||||
|
||||
const documentNo = order.workOrderNo || 'ABC123';
|
||||
const primaryAssignee = order.assignees?.find(a => a.isPrimary)?.name || order.assignee || '-';
|
||||
const items = order.items || [];
|
||||
|
||||
const formattedDueDate = order.dueDate !== '-'
|
||||
? new Date(order.dueDate).toLocaleDateString('ko-KR', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
}).replace(/\. /g, '-').replace('.', '')
|
||||
: '-';
|
||||
|
||||
// 규격 사이즈 컬럼
|
||||
const SCREEN_SIZES = ['1220', '900', '600', '400', '300'];
|
||||
|
||||
return (
|
||||
<div className="p-6 bg-white">
|
||||
{/* ===== 헤더 영역 ===== */}
|
||||
<div className="flex justify-between items-start mb-6">
|
||||
{/* 좌측: 제목 + 문서번호 */}
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">작업일지 (스크린)</h1>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
문서번호: {documentNo} | 작성일자: {fullDate}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 우측: 결재란 */}
|
||||
<table className="border-collapse text-sm flex-shrink-0">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="border border-gray-400 px-4 py-1 text-center align-middle" rowSpan={3}>결<br/>재</td>
|
||||
<td className="border border-gray-400 px-6 py-1 text-center">작성</td>
|
||||
<td className="border border-gray-400 px-6 py-1 text-center">승인</td>
|
||||
<td className="border border-gray-400 px-6 py-1 text-center">승인</td>
|
||||
<td className="border border-gray-400 px-6 py-1 text-center">승인</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border border-gray-400 px-6 py-3 text-center">{primaryAssignee}</td>
|
||||
<td className="border border-gray-400 px-6 py-3 text-center text-gray-400">이름</td>
|
||||
<td className="border border-gray-400 px-6 py-3 text-center text-gray-400">이름</td>
|
||||
<td className="border border-gray-400 px-6 py-3 text-center text-gray-400">이름</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border border-gray-400 px-6 py-1 text-center">부서명</td>
|
||||
<td className="border border-gray-400 px-6 py-1 text-center">부서명</td>
|
||||
<td className="border border-gray-400 px-6 py-1 text-center">부서명</td>
|
||||
<td className="border border-gray-400 px-6 py-1 text-center">부서명</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* ===== 신청업체 / 신청내용 ===== */}
|
||||
<table className="w-full border-collapse text-xs mb-6">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="border border-gray-400 bg-gray-100 px-3 py-2 text-center" colSpan={2}>신청업체</th>
|
||||
<th className="border border-gray-400 bg-gray-100 px-3 py-2 text-center" colSpan={2}>신청내용</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<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">{order.salesOrderDate || '-'}</td>
|
||||
<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">{order.projectName}</td>
|
||||
</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">{order.client}</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">{today}</td>
|
||||
</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">{primaryAssignee}</td>
|
||||
<td className="border border-gray-400 bg-gray-50 px-3 py-2 font-medium">제품 LOT NO</td>
|
||||
<td className="border border-gray-400 px-3 py-2">{order.lotNo}</td>
|
||||
</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">-</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">{primaryAssignee}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border border-gray-400 bg-gray-50 px-3 py-2 font-medium" colSpan={2}></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">{formattedDueDate}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{/* ===== 작업내역 ===== */}
|
||||
<SectionHeader variant="dark">작업내역</SectionHeader>
|
||||
<table className="w-full border-collapse text-xs mb-6">
|
||||
<thead>
|
||||
<tr className="bg-gray-100">
|
||||
<th className="border border-gray-400 p-2 w-8" rowSpan={2}>No.</th>
|
||||
<th className="border border-gray-400 p-2 w-20" rowSpan={2}>입고 LOT<br/>NO</th>
|
||||
<th className="border border-gray-400 p-2" rowSpan={2}>제품명</th>
|
||||
<th className="border border-gray-400 p-2 w-12" rowSpan={2}>부호</th>
|
||||
<th className="border border-gray-400 p-2" colSpan={2}>제작사이즈</th>
|
||||
<th className="border border-gray-400 p-2 w-12" rowSpan={2}>나머지<br/>높이</th>
|
||||
<th className="border border-gray-400 p-2" colSpan={5}>규격 (매수)</th>
|
||||
<th className="border border-gray-400 p-2 w-14" rowSpan={2}>제작<br/>형태</th>
|
||||
<th className="border border-gray-400 p-2 w-12" rowSpan={2}>제단<br/>사항</th>
|
||||
<th className="border border-gray-400 p-2 w-14" rowSpan={2}>작업<br/>완료</th>
|
||||
</tr>
|
||||
<tr className="bg-gray-100">
|
||||
<th className="border border-gray-400 p-2 w-12">가로</th>
|
||||
<th className="border border-gray-400 p-2 w-12">세로</th>
|
||||
{SCREEN_SIZES.map(size => (
|
||||
<th key={size} className="border border-gray-400 p-1 w-10">{size}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{items.length > 0 ? (
|
||||
items.map((item, idx) => (
|
||||
<tr key={item.id}>
|
||||
<td className="border border-gray-400 p-2 text-center">{idx + 1}</td>
|
||||
<td className="border border-gray-400 p-2 text-center">{order.lotNo}</td>
|
||||
<td className="border border-gray-400 p-2">{item.productName}</td>
|
||||
<td className="border border-gray-400 p-2 text-center">{item.floorCode}</td>
|
||||
<td className="border border-gray-400 p-2 text-center">-</td>
|
||||
<td className="border border-gray-400 p-2 text-center">-</td>
|
||||
<td className="border border-gray-400 p-2 text-center">-</td>
|
||||
{SCREEN_SIZES.map(size => (
|
||||
<td key={size} className="border border-gray-400 p-1 text-center">-</td>
|
||||
))}
|
||||
<td className="border border-gray-400 p-2 text-center">-</td>
|
||||
<td className="border border-gray-400 p-2 text-center">-</td>
|
||||
<td className="border border-gray-400 p-2 text-center">-</td>
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan={15} className="border border-gray-400 p-4 text-center text-gray-400">
|
||||
등록된 품목이 없습니다.
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{/* 합계 행 */}
|
||||
<tr className="bg-gray-50 font-medium">
|
||||
<td className="border border-gray-400 p-2 text-center" colSpan={4}>합계</td>
|
||||
<td className="border border-gray-400 p-2 text-center">-</td>
|
||||
<td className="border border-gray-400 p-2 text-center">-</td>
|
||||
<td className="border border-gray-400 p-2 text-center">-</td>
|
||||
{SCREEN_SIZES.map(size => (
|
||||
<td key={size} className="border border-gray-400 p-1 text-center">-</td>
|
||||
))}
|
||||
<td className="border border-gray-400 p-2 text-center">-</td>
|
||||
<td className="border border-gray-400 p-2 text-center">-</td>
|
||||
<td className="border border-gray-400 p-2 text-center">-</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{/* ===== 내화실 입고 LOT NO ===== */}
|
||||
<table className="w-full border-collapse text-xs mb-6">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="border border-gray-400 bg-gray-100 px-3 py-2 font-medium w-40">내화실 입고 LOT NO</td>
|
||||
<td className="border border-gray-400 px-3 py-2 min-h-[32px]"> </td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{/* ===== 비고 ===== */}
|
||||
<table className="w-full border-collapse text-xs">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="border border-gray-400 bg-gray-100 px-3 py-2 font-medium w-40 align-top">비고</td>
|
||||
<td className="border border-gray-400 px-3 py-3 min-h-[60px]">
|
||||
{order.note || ''}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,342 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 슬랫 중간검사 성적서 문서 콘텐츠
|
||||
*
|
||||
* 기획서 기준:
|
||||
* - 헤더: "중간검사성적서 (슬랫)" + 결재란
|
||||
* - 기본정보: 제품명/슬랫, 규격/EGI 1.6T, 수주처, 현장명 | 제품LOT NO, 로트크기, 검사일자, 검사자
|
||||
* - ■ 중간검사 기준서: 도해 + 검사항목/검사기준/검사방법/검사주기/관련규정
|
||||
* 가공상태, 결모양, 조립상태, 치수(높이/길이)
|
||||
* - ■ 중간검사 DATA: No, 가공상태결모양(양호/불량), 조립상태결모양(양호/불량),
|
||||
* ①높이(기준치/측정값입력), ②높이(기준치/측정값입력),
|
||||
* 길이(엔드락제외)(도면치수/측정값입력), 판정(자동)
|
||||
* - 부적합 내용 / 종합판정(자동)
|
||||
*/
|
||||
|
||||
import { useState, useCallback, useMemo } from 'react';
|
||||
import type { WorkOrder } from '../types';
|
||||
|
||||
interface SlatInspectionContentProps {
|
||||
data: WorkOrder;
|
||||
readOnly?: boolean;
|
||||
}
|
||||
|
||||
type CheckStatus = '양호' | '불량' | null;
|
||||
|
||||
interface InspectionRow {
|
||||
id: number;
|
||||
processStatus: CheckStatus; // 가공상태 결모양
|
||||
assemblyStatus: CheckStatus; // 조립상태 결모양
|
||||
height1Standard: string; // ① 높이 기준치 (표시용)
|
||||
height1Measured: string; // ① 높이 측정값 (입력)
|
||||
height2Standard: string; // ② 높이 기준치 (표시용)
|
||||
height2Measured: string; // ② 높이 측정값 (입력)
|
||||
lengthDesign: string; // 길이 도면치수 (입력)
|
||||
lengthMeasured: string; // 길이 측정값 (입력)
|
||||
}
|
||||
|
||||
const DEFAULT_ROW_COUNT = 6;
|
||||
|
||||
export function SlatInspectionContent({ data: order, readOnly = false }: SlatInspectionContentProps) {
|
||||
const fullDate = new Date().toLocaleDateString('ko-KR', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
});
|
||||
|
||||
const today = new Date().toLocaleDateString('ko-KR', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
}).replace(/\. /g, '-').replace('.', '');
|
||||
|
||||
const documentNo = order.workOrderNo || 'ABC123';
|
||||
const primaryAssignee = order.assignees?.find(a => a.isPrimary)?.name || order.assignee || '-';
|
||||
|
||||
const [rows, setRows] = useState<InspectionRow[]>(() =>
|
||||
Array.from({ length: DEFAULT_ROW_COUNT }, (_, i) => ({
|
||||
id: i + 1,
|
||||
processStatus: null,
|
||||
assemblyStatus: null,
|
||||
height1Standard: '16.5 ± 1',
|
||||
height1Measured: '',
|
||||
height2Standard: '14.5 ± 1',
|
||||
height2Measured: '',
|
||||
lengthDesign: '0',
|
||||
lengthMeasured: '',
|
||||
}))
|
||||
);
|
||||
|
||||
const [inadequateContent, setInadequateContent] = useState('');
|
||||
|
||||
const handleStatusChange = useCallback((rowId: number, field: 'processStatus' | 'assemblyStatus', value: CheckStatus) => {
|
||||
if (readOnly) return;
|
||||
setRows(prev => prev.map(row =>
|
||||
row.id === rowId ? { ...row, [field]: value } : row
|
||||
));
|
||||
}, [readOnly]);
|
||||
|
||||
const handleInputChange = useCallback((rowId: number, field: keyof InspectionRow, value: string) => {
|
||||
if (readOnly) return;
|
||||
setRows(prev => prev.map(row =>
|
||||
row.id === rowId ? { ...row, [field]: value } : row
|
||||
));
|
||||
}, [readOnly]);
|
||||
|
||||
// 행별 판정 자동 계산
|
||||
const getRowJudgment = useCallback((row: InspectionRow): '적' | '부' | null => {
|
||||
const { processStatus, assemblyStatus } = row;
|
||||
if (processStatus === '불량' || assemblyStatus === '불량') return '부';
|
||||
if (processStatus === '양호' && assemblyStatus === '양호') return '적';
|
||||
return null;
|
||||
}, []);
|
||||
|
||||
// 종합판정 자동 계산
|
||||
const overallResult = useMemo(() => {
|
||||
const judgments = rows.map(getRowJudgment);
|
||||
if (judgments.some(j => j === '부')) return '불합격';
|
||||
if (judgments.every(j => j === '적')) return '합격';
|
||||
return null;
|
||||
}, [rows, getRowJudgment]);
|
||||
|
||||
// 체크박스 렌더 (양호/불량)
|
||||
const renderCheckStatus = (rowId: number, field: 'processStatus' | 'assemblyStatus', value: CheckStatus) => (
|
||||
<td className="border border-gray-400 p-1">
|
||||
<div className="flex flex-col items-center gap-0.5">
|
||||
<label className="flex items-center gap-0.5 cursor-pointer text-xs">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={value === '양호'}
|
||||
onChange={() => handleStatusChange(rowId, field, value === '양호' ? null : '양호')}
|
||||
disabled={readOnly}
|
||||
className="w-3 h-3"
|
||||
/>
|
||||
양호
|
||||
</label>
|
||||
<label className="flex items-center gap-0.5 cursor-pointer text-xs">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={value === '불량'}
|
||||
onChange={() => handleStatusChange(rowId, field, value === '불량' ? null : '불량')}
|
||||
disabled={readOnly}
|
||||
className="w-3 h-3"
|
||||
/>
|
||||
불량
|
||||
</label>
|
||||
</div>
|
||||
</td>
|
||||
);
|
||||
|
||||
const inputClass = 'w-full text-center border-0 bg-transparent focus:outline-none focus:ring-1 focus:ring-blue-500 rounded text-xs';
|
||||
|
||||
return (
|
||||
<div className="p-6 bg-white">
|
||||
{/* ===== 헤더 영역 ===== */}
|
||||
<div className="flex justify-between items-start mb-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">중간검사성적서 (슬랫)</h1>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
문서번호: {documentNo} | 작성일자: {fullDate}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 결재란 */}
|
||||
<table className="border-collapse text-sm flex-shrink-0">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="border border-gray-400 px-4 py-1 text-center align-middle" rowSpan={3}>결<br/>재</td>
|
||||
<td className="border border-gray-400 px-6 py-1 text-center">작성</td>
|
||||
<td className="border border-gray-400 px-6 py-1 text-center">승인</td>
|
||||
<td className="border border-gray-400 px-6 py-1 text-center">승인</td>
|
||||
<td className="border border-gray-400 px-6 py-1 text-center">승인</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border border-gray-400 px-6 py-3 text-center">{primaryAssignee}</td>
|
||||
<td className="border border-gray-400 px-6 py-3 text-center text-gray-400">이름</td>
|
||||
<td className="border border-gray-400 px-6 py-3 text-center text-gray-400">이름</td>
|
||||
<td className="border border-gray-400 px-6 py-3 text-center text-gray-400">이름</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border border-gray-400 px-6 py-1 text-center">부서명</td>
|
||||
<td className="border border-gray-400 px-6 py-1 text-center">부서명</td>
|
||||
<td className="border border-gray-400 px-6 py-1 text-center">부서명</td>
|
||||
<td className="border border-gray-400 px-6 py-1 text-center">부서명</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* ===== 기본 정보 ===== */}
|
||||
<table className="w-full border-collapse text-xs mb-6">
|
||||
<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 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>
|
||||
<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">EGI 1.6T</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?.length || 0} 개소</td>
|
||||
</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">{order.client || '-'}</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">{today}</td>
|
||||
</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">{order.projectName || '-'}</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">{primaryAssignee}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{/* ===== 중간검사 기준서 ===== */}
|
||||
<div className="mb-1 font-bold text-sm">■ 중간검사 기준서</div>
|
||||
<table className="w-full border-collapse text-xs mb-6">
|
||||
<tbody>
|
||||
<tr>
|
||||
{/* 도해 영역 */}
|
||||
<td className="border border-gray-400 p-4 text-center text-gray-300 align-middle w-1/5" rowSpan={7}>
|
||||
<div className="h-40 flex items-center justify-center">도해 이미지 영역</div>
|
||||
</td>
|
||||
{/* 헤더 행 */}
|
||||
<th className="border border-gray-400 bg-gray-100 px-2 py-1 text-center" colSpan={3}>검사항목</th>
|
||||
<th className="border border-gray-400 bg-gray-100 px-2 py-1 text-center">검사기준</th>
|
||||
<th className="border border-gray-400 bg-gray-100 px-2 py-1 text-center">검사방법</th>
|
||||
<th className="border border-gray-400 bg-gray-100 px-2 py-1 text-center">검사주기</th>
|
||||
<th className="border border-gray-400 bg-gray-100 px-2 py-1 text-center">관련규정</th>
|
||||
</tr>
|
||||
{/* 결모양 > 가공상태 */}
|
||||
<tr>
|
||||
<td className="border border-gray-400 px-2 py-1 font-medium text-center" rowSpan={3}>결모양</td>
|
||||
<td className="border border-gray-400 px-2 py-1 font-medium" colSpan={2}>가공상태</td>
|
||||
<td className="border border-gray-400 px-2 py-1">사용상 해로운 결함이 없을것</td>
|
||||
<td className="border border-gray-400 px-2 py-1 text-center" rowSpan={3}>육안검사</td>
|
||||
<td className="border border-gray-400 px-2 py-1 text-center"></td>
|
||||
<td className="border border-gray-400 px-2 py-1">KS F 4510 5.1항</td>
|
||||
</tr>
|
||||
{/* 결모양 > 조립상태 (상단) */}
|
||||
<tr>
|
||||
<td className="border border-gray-400 px-2 py-1 font-medium" colSpan={2} rowSpan={2}>조립상태</td>
|
||||
<td className="border border-gray-400 px-2 py-1" rowSpan={2}>엔드락이 용접에 의해<br/>견고하게 조립되어야 함<br/>용접부위에 락카도색이<br/>되어야 함</td>
|
||||
<td className="border border-gray-400 px-2 py-1 text-center" rowSpan={2}>n = 1, c = 0</td>
|
||||
<td className="border border-gray-400 px-2 py-1">KS F 4510 9항</td>
|
||||
</tr>
|
||||
{/* 결모양 > 조립상태 (하단 - 자체규정) */}
|
||||
<tr>
|
||||
<td className="border border-gray-400 px-2 py-1">자체규정</td>
|
||||
</tr>
|
||||
{/* 치수 > 높이 > ① */}
|
||||
<tr>
|
||||
<td className="border border-gray-400 px-2 py-1 font-medium bg-gray-50 text-center" rowSpan={3}>치수<br/>(mm)</td>
|
||||
<td className="border border-gray-400 px-2 py-1 font-medium text-center" rowSpan={2}>높이</td>
|
||||
<td className="border border-gray-400 px-2 py-1 text-center">①</td>
|
||||
<td className="border border-gray-400 px-2 py-1 text-center">16.5 ± 1</td>
|
||||
<td className="border border-gray-400 px-2 py-1 text-center" rowSpan={3}>체크검사</td>
|
||||
<td className="border border-gray-400 px-2 py-1 text-center" rowSpan={3}></td>
|
||||
<td className="border border-gray-400 px-2 py-1" rowSpan={3}>KS F 4510 7항<br/>표9</td>
|
||||
</tr>
|
||||
{/* 치수 > 높이 > ② */}
|
||||
<tr>
|
||||
<td className="border border-gray-400 px-2 py-1 text-center">②</td>
|
||||
<td className="border border-gray-400 px-2 py-1 text-center">14.5 ± 1</td>
|
||||
</tr>
|
||||
{/* 치수 > 길이 > ③ */}
|
||||
<tr>
|
||||
<td className="border border-gray-400 px-2 py-1 font-medium text-center">길이</td>
|
||||
<td className="border border-gray-400 px-2 py-1 text-center">③</td>
|
||||
<td className="border border-gray-400 px-2 py-1 text-center">도면치수(엔드락제외) ± 4</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{/* ===== 중간검사 DATA ===== */}
|
||||
<div className="mb-1 font-bold text-sm">■ 중간검사 DATA</div>
|
||||
<table className="w-full border-collapse text-xs mb-4">
|
||||
<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-16" rowSpan={2}>가공상태<br/>결모양</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>
|
||||
<th className="border border-gray-400 p-1" colSpan={2}>길이 (mm)<br/><span className="font-normal text-gray-500">(엔드락 제외)</span></th>
|
||||
<th className="border border-gray-400 p-1 w-14" rowSpan={2}>판정<br/>(적/부)</th>
|
||||
</tr>
|
||||
<tr className="bg-gray-100">
|
||||
<th className="border border-gray-400 p-1 w-16">기준치</th>
|
||||
<th className="border border-gray-400 p-1 w-16">측정값</th>
|
||||
<th className="border border-gray-400 p-1 w-16">기준치</th>
|
||||
<th className="border border-gray-400 p-1 w-16">측정값</th>
|
||||
<th className="border border-gray-400 p-1 w-16">도면치수</th>
|
||||
<th className="border border-gray-400 p-1 w-16">측정값</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.map((row) => {
|
||||
const judgment = getRowJudgment(row);
|
||||
return (
|
||||
<tr key={row.id}>
|
||||
<td className="border border-gray-400 p-1 text-center">{row.id}</td>
|
||||
{/* 가공상태 - 양호/불량 체크 */}
|
||||
{renderCheckStatus(row.id, 'processStatus', row.processStatus)}
|
||||
{/* 조립상태 - 양호/불량 체크 */}
|
||||
{renderCheckStatus(row.id, 'assemblyStatus', row.assemblyStatus)}
|
||||
{/* ① 높이 - 기준치 표시 + 측정값 입력 */}
|
||||
<td className="border border-gray-400 p-1 text-center">{row.height1Standard}</td>
|
||||
<td className="border border-gray-400 p-1">
|
||||
<input type="text" value={row.height1Measured} onChange={(e) => handleInputChange(row.id, 'height1Measured', e.target.value)} disabled={readOnly} className={inputClass} placeholder="-" />
|
||||
</td>
|
||||
{/* ② 높이 - 기준치 표시 + 측정값 입력 */}
|
||||
<td className="border border-gray-400 p-1 text-center">{row.height2Standard}</td>
|
||||
<td className="border border-gray-400 p-1">
|
||||
<input type="text" value={row.height2Measured} onChange={(e) => handleInputChange(row.id, 'height2Measured', e.target.value)} disabled={readOnly} className={inputClass} placeholder="-" />
|
||||
</td>
|
||||
{/* 길이 (엔드락 제외) - 도면치수 표시 (입력 불가) + 측정값 입력 */}
|
||||
<td className="border border-gray-400 p-1 text-center">{row.lengthDesign || '-'}</td>
|
||||
<td className="border border-gray-400 p-1">
|
||||
<input type="text" value={row.lengthMeasured} onChange={(e) => handleInputChange(row.id, 'lengthMeasured', e.target.value)} disabled={readOnly} className={inputClass} placeholder="-" />
|
||||
</td>
|
||||
{/* 판정 - 자동 계산 */}
|
||||
<td className={`border border-gray-400 p-1 text-center font-bold ${
|
||||
judgment === '적' ? 'text-blue-600' : judgment === '부' ? 'text-red-600' : 'text-gray-300'
|
||||
}`}>
|
||||
{judgment || '-'}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{/* ===== 부적합 내용 + 종합판정 ===== */}
|
||||
<table className="w-full border-collapse text-xs">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="border border-gray-400 bg-gray-100 px-3 py-2 font-medium w-24 align-top">부적합 내용</td>
|
||||
<td className="border border-gray-400 px-3 py-2" colSpan={2}>
|
||||
<textarea value={inadequateContent} onChange={(e) => !readOnly && setInadequateContent(e.target.value)} disabled={readOnly}
|
||||
className="w-full border-0 bg-transparent focus:outline-none focus:ring-1 focus:ring-blue-500 rounded text-xs resize-none" rows={2} />
|
||||
</td>
|
||||
<td className="border border-gray-400 bg-gray-100 px-3 py-2 font-medium text-center w-24">종합판정</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border border-gray-400 px-3 py-2" colSpan={3}></td>
|
||||
<td className={`border border-gray-400 px-3 py-2 text-center font-bold text-sm ${
|
||||
overallResult === '합격' ? 'text-blue-600' : overallResult === '불합격' ? 'text-red-600' : 'text-gray-400'
|
||||
}`}>
|
||||
{overallResult || '합격'}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 슬랫 작업일지 문서 콘텐츠
|
||||
*
|
||||
* 기획서 기준 구성:
|
||||
* - 헤더: "작업일지 (슬랫)" + 문서번호/작성일자 + 결재란(결재|작성/승인/승인/승인)
|
||||
* - 신청업체(수주일,수주처,담당자,연락처) / 신청내용(현장명,작업일자,제품LOT NO,생산담당자,출고예정일)
|
||||
* - 작업내역: No, 입고 LOT NO, 방화유리 수량, 제품명,
|
||||
* 제작사이즈(mm)-미미제외(가로/세로/매수(세로)), 조인트바 수량, 코일 사용량, 설치홈/부호
|
||||
* - 생산량 합계[m²] / 조인트바 합계
|
||||
* - 비고
|
||||
*/
|
||||
|
||||
import type { WorkOrder } from '../types';
|
||||
import { SectionHeader } from '@/components/document-system';
|
||||
|
||||
interface SlatWorkLogContentProps {
|
||||
data: WorkOrder;
|
||||
}
|
||||
|
||||
export function SlatWorkLogContent({ data: order }: SlatWorkLogContentProps) {
|
||||
const today = new Date().toLocaleDateString('ko-KR', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
}).replace(/\. /g, '-').replace('.', '');
|
||||
|
||||
const fullDate = new Date().toLocaleDateString('ko-KR', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
});
|
||||
|
||||
const documentNo = order.workOrderNo || 'ABC123';
|
||||
const primaryAssignee = order.assignees?.find(a => a.isPrimary)?.name || order.assignee || '-';
|
||||
const items = order.items || [];
|
||||
|
||||
const formattedDueDate = order.dueDate !== '-'
|
||||
? new Date(order.dueDate).toLocaleDateString('ko-KR', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
}).replace(/\. /g, '-').replace('.', '')
|
||||
: '-';
|
||||
|
||||
return (
|
||||
<div className="p-6 bg-white">
|
||||
{/* ===== 헤더 영역 ===== */}
|
||||
<div className="flex justify-between items-start mb-6">
|
||||
{/* 좌측: 제목 + 문서번호 */}
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">작업일지 (슬랫)</h1>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
문서번호: {documentNo} | 작성일자: {fullDate}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 우측: 결재란 */}
|
||||
<table className="border-collapse text-sm flex-shrink-0">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="border border-gray-400 px-4 py-1 text-center align-middle" rowSpan={3}>결<br/>재</td>
|
||||
<td className="border border-gray-400 px-6 py-1 text-center">작성</td>
|
||||
<td className="border border-gray-400 px-6 py-1 text-center">승인</td>
|
||||
<td className="border border-gray-400 px-6 py-1 text-center">승인</td>
|
||||
<td className="border border-gray-400 px-6 py-1 text-center">승인</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border border-gray-400 px-6 py-3 text-center">{primaryAssignee}</td>
|
||||
<td className="border border-gray-400 px-6 py-3 text-center text-gray-400">이름</td>
|
||||
<td className="border border-gray-400 px-6 py-3 text-center text-gray-400">이름</td>
|
||||
<td className="border border-gray-400 px-6 py-3 text-center text-gray-400">이름</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border border-gray-400 px-6 py-1 text-center">부서명</td>
|
||||
<td className="border border-gray-400 px-6 py-1 text-center">부서명</td>
|
||||
<td className="border border-gray-400 px-6 py-1 text-center">부서명</td>
|
||||
<td className="border border-gray-400 px-6 py-1 text-center">부서명</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* ===== 신청업체 / 신청내용 ===== */}
|
||||
<table className="w-full border-collapse text-xs mb-6">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="border border-gray-400 bg-gray-100 px-3 py-2 text-center" colSpan={2}>신청업체</th>
|
||||
<th className="border border-gray-400 bg-gray-100 px-3 py-2 text-center" colSpan={2}>신청내용</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<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">{order.salesOrderDate || '-'}</td>
|
||||
<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">{order.projectName}</td>
|
||||
</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">{order.client}</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">{today}</td>
|
||||
</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">{primaryAssignee}</td>
|
||||
<td className="border border-gray-400 bg-gray-50 px-3 py-2 font-medium">제품 LOT NO</td>
|
||||
<td className="border border-gray-400 px-3 py-2">{order.lotNo}</td>
|
||||
</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">-</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">{primaryAssignee}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border border-gray-400 bg-gray-50 px-3 py-2 font-medium" colSpan={2}></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">{formattedDueDate}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{/* ===== 작업내역 ===== */}
|
||||
<SectionHeader variant="dark">작업내역</SectionHeader>
|
||||
<table className="w-full border-collapse text-xs mb-6">
|
||||
<thead>
|
||||
<tr className="bg-gray-100">
|
||||
<th className="border border-gray-400 p-2 w-8" rowSpan={2}>No.</th>
|
||||
<th className="border border-gray-400 p-2 w-24" rowSpan={2}>입고 LOT<br/>NO</th>
|
||||
<th className="border border-gray-400 p-2 w-16" rowSpan={2}>방화유리<br/>수량</th>
|
||||
<th className="border border-gray-400 p-2" rowSpan={2}>제품명</th>
|
||||
<th className="border border-gray-400 p-2" colSpan={3}>제작사이즈(mm) - 미미제외</th>
|
||||
<th className="border border-gray-400 p-2 w-16" rowSpan={2}>조인트바<br/>수량</th>
|
||||
<th className="border border-gray-400 p-2 w-16" rowSpan={2}>코일<br/>사용량</th>
|
||||
<th className="border border-gray-400 p-2 w-16" rowSpan={2}>설치홈/<br/>부호</th>
|
||||
</tr>
|
||||
<tr className="bg-gray-100">
|
||||
<th className="border border-gray-400 p-2 w-14">가로</th>
|
||||
<th className="border border-gray-400 p-2 w-14">세로</th>
|
||||
<th className="border border-gray-400 p-2 w-14">매수<br/>(세로)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{items.length > 0 ? (
|
||||
items.map((item, idx) => (
|
||||
<tr key={item.id}>
|
||||
<td className="border border-gray-400 p-2 text-center">{idx + 1}</td>
|
||||
<td className="border border-gray-400 p-2 text-center">{order.lotNo}</td>
|
||||
<td className="border border-gray-400 p-2 text-center">-</td>
|
||||
<td className="border border-gray-400 p-2">{item.productName}</td>
|
||||
<td className="border border-gray-400 p-2 text-center">-</td>
|
||||
<td className="border border-gray-400 p-2 text-center">-</td>
|
||||
<td className="border border-gray-400 p-2 text-center">-</td>
|
||||
<td className="border border-gray-400 p-2 text-center">-</td>
|
||||
<td className="border border-gray-400 p-2 text-center">-</td>
|
||||
<td className="border border-gray-400 p-2 text-center">-</td>
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan={10} className="border border-gray-400 p-4 text-center text-gray-400">
|
||||
등록된 품목이 없습니다.
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{/* ===== 생산량 합계 / 조인트바 합계 ===== */}
|
||||
<table className="w-full border-collapse text-xs mb-6">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="border border-gray-400 bg-gray-100 px-3 py-2 font-medium w-36">생산량 합계 [m²]</td>
|
||||
<td className="border border-gray-400 px-3 py-2"></td>
|
||||
<td className="border border-gray-400 bg-gray-100 px-3 py-2 font-medium w-36">조인트바 합계</td>
|
||||
<td className="border border-gray-400 px-3 py-2"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{/* ===== 비고 ===== */}
|
||||
<table className="w-full border-collapse text-xs">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="border border-gray-400 bg-gray-100 px-3 py-2 font-medium w-40 align-top">비고</td>
|
||||
<td className="border border-gray-400 px-3 py-3 min-h-[60px]">
|
||||
{order.note || ''}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
12
src/components/production/WorkOrders/documents/index.ts
Normal file
12
src/components/production/WorkOrders/documents/index.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
// 작업일지 문서 (공정별)
|
||||
export { ScreenWorkLogContent } from './ScreenWorkLogContent';
|
||||
export { SlatWorkLogContent } from './SlatWorkLogContent';
|
||||
export { BendingWorkLogContent } from './BendingWorkLogContent';
|
||||
|
||||
// 중간검사 성적서 문서 (공정별)
|
||||
export { ScreenInspectionContent } from './ScreenInspectionContent';
|
||||
export { SlatInspectionContent } from './SlatInspectionContent';
|
||||
export { BendingInspectionContent } from './BendingInspectionContent';
|
||||
|
||||
// 모달
|
||||
export { InspectionReportModal } from './InspectionReportModal';
|
||||
Reference in New Issue
Block a user