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:
@@ -8,7 +8,7 @@
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import { Check, X, ChevronDown, ChevronRight } from 'lucide-react';
|
||||
import { Check, ChevronDown, ChevronRight } from 'lucide-react';
|
||||
import { ContentSkeleton } from '@/components/ui/skeleton';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
@@ -270,16 +270,8 @@ export function AssigneeSelectModal({
|
||||
</VisuallyHidden>
|
||||
|
||||
{/* 헤더 */}
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b">
|
||||
<div className="px-6 py-4 border-b">
|
||||
<h2 className="text-lg font-semibold">담당자 선택</h2>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onOpenChange(false)}
|
||||
className="rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">닫기</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 설명 텍스트 */}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
import { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { FileText, Play, CheckCircle2, Loader2, Undo2, Pencil } from 'lucide-react';
|
||||
import { FileText, Play, CheckCircle2, Loader2, Undo2, ClipboardCheck } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import {
|
||||
@@ -23,13 +23,12 @@ import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetai
|
||||
import { workOrderConfig } from './workOrderConfig';
|
||||
import { ServerErrorPage } from '@/components/common/ServerErrorPage';
|
||||
import { WorkLogModal } from '../WorkerScreen/WorkLogModal';
|
||||
import { InspectionReportModal } from './documents';
|
||||
import { toast } from 'sonner';
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
import { getWorkOrderById, updateWorkOrderStatus, updateWorkOrderItemStatus, type WorkOrderItemStatus } from './actions';
|
||||
import { getWorkOrderById, updateWorkOrderStatus } from './actions';
|
||||
import {
|
||||
WORK_ORDER_STATUS_LABELS,
|
||||
WORK_ORDER_STATUS_COLORS,
|
||||
ITEM_STATUS_LABELS,
|
||||
ISSUE_STATUS_LABELS,
|
||||
SCREEN_PROCESS_STEPS,
|
||||
SLAT_PROCESS_STEPS,
|
||||
@@ -39,8 +38,8 @@ import {
|
||||
type ProcessStep,
|
||||
} from './types';
|
||||
|
||||
// 공정 진행 단계 컴포넌트
|
||||
function ProcessSteps({
|
||||
// 공정 진행 단계 (wrapper 없이 pills만 렌더링)
|
||||
function ProcessStepPills({
|
||||
processType,
|
||||
currentStep,
|
||||
workSteps,
|
||||
@@ -59,45 +58,26 @@ function ProcessSteps({
|
||||
: BENDING_PROCESS_STEPS;
|
||||
|
||||
if (steps.length === 0) {
|
||||
return (
|
||||
<div className="bg-white border rounded-lg p-6">
|
||||
<h3 className="font-semibold mb-4">공정 진행</h3>
|
||||
<p className="text-gray-500">공정 단계가 설정되지 않았습니다.</p>
|
||||
</div>
|
||||
);
|
||||
return <p className="text-gray-500">공정 단계가 설정되지 않았습니다.</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-white border rounded-lg p-6">
|
||||
<h3 className="font-semibold mb-4">공정 진행 ({steps.length}단계)</h3>
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
{steps.map((step, index) => {
|
||||
const isCompleted = index < currentStep;
|
||||
const isCurrent = index === currentStep;
|
||||
<div className="flex items-center gap-3 flex-wrap">
|
||||
{steps.map((step, index) => {
|
||||
const isCompleted = index < currentStep;
|
||||
|
||||
return (
|
||||
<div key={step.key || `step-${index}`} className="flex items-center">
|
||||
<div
|
||||
className={`flex items-center gap-2 px-4 py-2 rounded-full border ${
|
||||
isCompleted
|
||||
? 'bg-gray-900 text-white border-gray-900'
|
||||
: isCurrent
|
||||
? 'bg-white border-gray-900 text-gray-900'
|
||||
: 'bg-white border-gray-300 text-gray-500'
|
||||
}`}
|
||||
>
|
||||
<span className="font-medium">{step.order}</span>
|
||||
<span>{step.label}</span>
|
||||
{isCompleted && (
|
||||
<span className="text-xs bg-white text-gray-900 px-1.5 py-0.5 rounded">
|
||||
완료
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
return (
|
||||
<div
|
||||
key={step.key || `step-${index}`}
|
||||
className="flex items-center gap-2 px-4 py-2 rounded-full bg-gray-900 text-white border border-gray-900"
|
||||
>
|
||||
<span>{step.label}</span>
|
||||
{isCompleted && (
|
||||
<span className="text-green-400 font-medium">완료</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -206,10 +186,10 @@ interface WorkOrderDetailProps {
|
||||
export function WorkOrderDetail({ orderId }: WorkOrderDetailProps) {
|
||||
const router = useRouter();
|
||||
const [isWorkLogOpen, setIsWorkLogOpen] = useState(false);
|
||||
const [isInspectionOpen, setIsInspectionOpen] = useState(false);
|
||||
const [order, setOrder] = useState<WorkOrder | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isStatusUpdating, setIsStatusUpdating] = useState(false);
|
||||
const [updatingItemId, setUpdatingItemId] = useState<number | null>(null);
|
||||
|
||||
// 수정 모드로 이동 핸들러
|
||||
const handleEdit = useCallback(() => {
|
||||
@@ -264,49 +244,6 @@ export function WorkOrderDetail({ orderId }: WorkOrderDetailProps) {
|
||||
}
|
||||
}, [order, orderId]);
|
||||
|
||||
// 품목 상태 변경 핸들러
|
||||
const handleItemStatusChange = useCallback(async (itemId: number, newStatus: WorkOrderItemStatus) => {
|
||||
if (!order) return;
|
||||
|
||||
setUpdatingItemId(itemId);
|
||||
try {
|
||||
const result = await updateWorkOrderItemStatus(orderId, itemId, newStatus);
|
||||
if (result.success) {
|
||||
// 로컬 상태 업데이트 (품목 + 작업지시 상태)
|
||||
setOrder(prev => {
|
||||
if (!prev) return prev;
|
||||
return {
|
||||
...prev,
|
||||
status: result.workOrderStatus || prev.status,
|
||||
items: prev.items.map(item =>
|
||||
item.id === itemId ? { ...item, status: newStatus } : item
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
const statusLabels: Record<WorkOrderItemStatus, string> = {
|
||||
waiting: '대기',
|
||||
in_progress: '작업중',
|
||||
completed: '완료',
|
||||
};
|
||||
toast.success(`품목 상태가 '${statusLabels[newStatus]}'(으)로 변경되었습니다.`);
|
||||
|
||||
// 작업지시 상태가 변경된 경우 추가 알림
|
||||
if (result.workOrderStatusChanged && result.workOrderStatus) {
|
||||
const workOrderStatusLabel = WORK_ORDER_STATUS_LABELS[result.workOrderStatus as keyof typeof WORK_ORDER_STATUS_LABELS] || result.workOrderStatus;
|
||||
toast.info(`작업지시 상태가 '${workOrderStatusLabel}'(으)로 자동 변경되었습니다.`);
|
||||
}
|
||||
} else {
|
||||
toast.error(result.error || '품목 상태 변경에 실패했습니다.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[WorkOrderDetail] handleItemStatusChange error:', error);
|
||||
toast.error('품목 상태 변경 중 오류가 발생했습니다.');
|
||||
} finally {
|
||||
setUpdatingItemId(null);
|
||||
}
|
||||
}, [order, orderId]);
|
||||
|
||||
// 커스텀 헤더 액션 (상태 변경 버튼, 작업일지 버튼)
|
||||
const customHeaderActions = useMemo(() => {
|
||||
if (!order) return null;
|
||||
@@ -374,7 +311,14 @@ export function WorkOrderDetail({ orderId }: WorkOrderDetailProps) {
|
||||
)}
|
||||
<Button variant="outline" onClick={() => setIsWorkLogOpen(true)}>
|
||||
<FileText className="w-4 h-4 mr-1.5" />
|
||||
작업일지
|
||||
작업일지 보기
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setIsInspectionOpen(true)}
|
||||
>
|
||||
<ClipboardCheck className="w-4 h-4 mr-1.5" />
|
||||
중간검사성적서 보기
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
@@ -386,30 +330,31 @@ export function WorkOrderDetail({ orderId }: WorkOrderDetailProps) {
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* 기본 정보 */}
|
||||
{/* 기본 정보 (기획서 4열 구성) */}
|
||||
<div className="bg-amber-50 border border-amber-200 rounded-lg p-6">
|
||||
<h3 className="font-semibold mb-4">기본 정보</h3>
|
||||
<div className="grid grid-cols-4 gap-y-4">
|
||||
<div className="grid grid-cols-4 gap-x-6 gap-y-4">
|
||||
{/* 1행: 작업번호 | 수주일 | 공정구분 | 로트번호 */}
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground mb-1">작업지시번호</p>
|
||||
<p className="text-sm text-muted-foreground mb-1">작업번호</p>
|
||||
<p className="font-medium">{order.workOrderNo}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground mb-1">로트번호</p>
|
||||
<p className="font-medium">{order.lotNo}</p>
|
||||
<p className="text-sm text-muted-foreground mb-1">수주일</p>
|
||||
<p className="font-medium">{order.salesOrderDate || '-'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground mb-1">공정구분</p>
|
||||
<p className="font-medium">{order.processName}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground mb-1">작업상태</p>
|
||||
<Badge className={`${WORK_ORDER_STATUS_COLORS[order.status]} border-0`}>
|
||||
{WORK_ORDER_STATUS_LABELS[order.status]}
|
||||
</Badge>
|
||||
<p className="text-sm text-muted-foreground mb-1">로트번호</p>
|
||||
<p className="font-medium">{order.lotNo}</p>
|
||||
</div>
|
||||
|
||||
{/* 2행: 수주처 | 현장명 | 수주 담당자 | 담당자 연락처 */}
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground mb-1">발주처</p>
|
||||
<p className="text-sm text-muted-foreground mb-1">수주처</p>
|
||||
<p className="font-medium">{order.client}</p>
|
||||
</div>
|
||||
<div>
|
||||
@@ -417,11 +362,35 @@ export function WorkOrderDetail({ orderId }: WorkOrderDetailProps) {
|
||||
<p className="font-medium">{order.projectName}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground mb-1">납기일</p>
|
||||
<p className="font-medium">{order.dueDate}</p>
|
||||
<p className="text-sm text-muted-foreground mb-1">수주 담당자</p>
|
||||
<p className="font-medium">-</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground mb-1">작업자</p>
|
||||
<p className="text-sm text-muted-foreground mb-1">담당자 연락처</p>
|
||||
<p className="font-medium">-</p>
|
||||
</div>
|
||||
|
||||
{/* 3행: 출고예정일 | 틀수 | 우선순위 | 부서 */}
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground mb-1">출고예정일</p>
|
||||
<p className="font-medium">{order.shipmentDate || '-'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground mb-1">틀수</p>
|
||||
<p className="font-medium">{order.shutterCount ?? '-'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground mb-1">우선순위</p>
|
||||
<p className="font-medium">{order.priorityLabel || '-'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground mb-1">부서</p>
|
||||
<p className="font-medium">{order.department || '-'}</p>
|
||||
</div>
|
||||
|
||||
{/* 4행: 생산 담당자 | 상태 | 비고 (colspan 2) */}
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground mb-1">생산 담당자</p>
|
||||
<p className="font-medium">
|
||||
{order.assignees && order.assignees.length > 0
|
||||
? order.assignees.map(a => a.name).join(', ')
|
||||
@@ -429,120 +398,65 @@ export function WorkOrderDetail({ orderId }: WorkOrderDetailProps) {
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground mb-1">우선순위</p>
|
||||
<p className="font-medium">
|
||||
{order.priority === 1 ? '1 (긴급)' : order.priority === 5 ? '5 (일반)' : order.priority || '-'}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground mb-1">상태</p>
|
||||
<Badge className={`${WORK_ORDER_STATUS_COLORS[order.status]} border-0`}>
|
||||
{WORK_ORDER_STATUS_LABELS[order.status]}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<p className="text-sm text-muted-foreground mb-1">비고</p>
|
||||
<p className="font-medium whitespace-pre-wrap">{order.note || '-'}</p>
|
||||
</div>
|
||||
{order.note && (
|
||||
<div className="col-span-4 mt-2 pt-4 border-t">
|
||||
<p className="text-sm text-muted-foreground mb-1">비고</p>
|
||||
<p className="font-medium whitespace-pre-wrap">{order.note}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 공정 진행 */}
|
||||
<ProcessSteps
|
||||
processType={order.processType}
|
||||
currentStep={order.currentStep}
|
||||
workSteps={order.workSteps}
|
||||
/>
|
||||
|
||||
{/* 작업 품목 */}
|
||||
{/* 공정 진행 + 작업 품목 (기획서 구조) */}
|
||||
<div className="bg-white border rounded-lg p-6">
|
||||
<h3 className="font-semibold mb-4">작업 품목 ({order.items.length}건)</h3>
|
||||
<h3 className="font-semibold mb-4">공정 진행</h3>
|
||||
|
||||
{/* 공정 단계 박스 (inner bordered box) */}
|
||||
<div className="border rounded-lg p-4 mb-6">
|
||||
{/* 품목 정보 헤더 (항상 표시) */}
|
||||
<p className="font-semibold mb-3">
|
||||
{order.items.length > 0 ? (
|
||||
<>
|
||||
{order.items[0].productName}
|
||||
{order.items[0].specification !== '-' ? ` ${order.items[0].specification}` : ''}
|
||||
{` ${order.items[0].quantity}${order.items[0].unit !== '-' ? order.items[0].unit : '개'}`}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{order.processCode} ({order.processName})
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
|
||||
{/* 공정 단계 pills */}
|
||||
<ProcessStepPills
|
||||
processType={order.processType}
|
||||
currentStep={order.currentStep}
|
||||
workSteps={order.workSteps}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 작업 품목 테이블 (로트번호 | 품목명 | 수량 | 단위) */}
|
||||
{order.items.length > 0 ? (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="bg-muted/50">
|
||||
<TableHead className="w-14">No</TableHead>
|
||||
<TableHead className="w-20">상태</TableHead>
|
||||
<TableHead>로트번호</TableHead>
|
||||
<TableHead>품목명</TableHead>
|
||||
<TableHead className="w-28">층/부호</TableHead>
|
||||
<TableHead className="w-32">규격</TableHead>
|
||||
<TableHead className="w-20 text-right">수량</TableHead>
|
||||
<TableHead className="w-20">작업</TableHead>
|
||||
<TableHead className="w-24 text-right">수량</TableHead>
|
||||
<TableHead className="w-20">단위</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{order.items.map((item) => (
|
||||
<TableRow key={item.id}>
|
||||
<TableCell>{item.no}</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="outline">{ITEM_STATUS_LABELS[item.status]}</Badge>
|
||||
</TableCell>
|
||||
<TableCell>{order.lotNo}</TableCell>
|
||||
<TableCell className="font-medium">{item.productName}</TableCell>
|
||||
<TableCell>{item.floorCode}</TableCell>
|
||||
<TableCell>{item.specification}</TableCell>
|
||||
<TableCell className="text-right">{item.quantity}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex gap-1">
|
||||
{item.status === 'waiting' && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleItemStatusChange(item.id, 'in_progress')}
|
||||
disabled={updatingItemId === item.id}
|
||||
>
|
||||
{updatingItemId === item.id ? (
|
||||
<Loader2 className="w-3 h-3 mr-1 animate-spin" />
|
||||
) : (
|
||||
<Play className="w-3 h-3 mr-1" />
|
||||
)}
|
||||
시작
|
||||
</Button>
|
||||
)}
|
||||
{item.status === 'in_progress' && (
|
||||
<>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleItemStatusChange(item.id, 'completed')}
|
||||
disabled={updatingItemId === item.id}
|
||||
>
|
||||
{updatingItemId === item.id ? (
|
||||
<Loader2 className="w-3 h-3 mr-1 animate-spin" />
|
||||
) : (
|
||||
<CheckCircle2 className="w-3 h-3 mr-1" />
|
||||
)}
|
||||
완료
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleItemStatusChange(item.id, 'waiting')}
|
||||
disabled={updatingItemId === item.id}
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
{updatingItemId === item.id ? (
|
||||
<Loader2 className="w-3 h-3 mr-1 animate-spin" />
|
||||
) : (
|
||||
<Undo2 className="w-3 h-3 mr-1" />
|
||||
)}
|
||||
취소
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{item.status === 'completed' && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleItemStatusChange(item.id, 'in_progress')}
|
||||
disabled={updatingItemId === item.id}
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
{updatingItemId === item.id ? (
|
||||
<Loader2 className="w-3 h-3 mr-1 animate-spin" />
|
||||
) : (
|
||||
<Undo2 className="w-3 h-3 mr-1" />
|
||||
)}
|
||||
되돌리기
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>{item.unit}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
@@ -589,12 +503,23 @@ export function WorkOrderDetail({ orderId }: WorkOrderDetailProps) {
|
||||
renderForm={() => renderFormContent()}
|
||||
/>
|
||||
|
||||
{/* 작업일지 모달 */}
|
||||
{/* 작업일지 모달 (공정별) */}
|
||||
{order && (
|
||||
<WorkLogModal
|
||||
open={isWorkLogOpen}
|
||||
onOpenChange={setIsWorkLogOpen}
|
||||
workOrderId={order.id}
|
||||
processType={order.processType}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 중간검사 성적서 모달 (공정별) */}
|
||||
{order && (
|
||||
<InspectionReportModal
|
||||
open={isInspectionOpen}
|
||||
onOpenChange={setIsInspectionOpen}
|
||||
workOrderId={order.id}
|
||||
processType={order.processType}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -193,19 +193,13 @@ export function WorkOrderEdit({ orderId }: WorkOrderEditProps) {
|
||||
router.back();
|
||||
};
|
||||
|
||||
// 선택된 공정의 코드 가져오기
|
||||
const getSelectedProcessCode = (): string => {
|
||||
const selectedProcess = processOptions.find(p => p.id === formData.processId);
|
||||
return selectedProcess?.processCode || '-';
|
||||
};
|
||||
|
||||
// 동적 config (작업지시 번호 포함)
|
||||
const dynamicConfig = {
|
||||
...workOrderEditConfig,
|
||||
title: workOrder ? `작업지시 (${workOrder.workOrderNo})` : '작업지시',
|
||||
};
|
||||
|
||||
// 폼 컨텐츠 렌더링
|
||||
// 폼 컨텐츠 렌더링 (기획서 4열 그리드)
|
||||
const renderFormContent = useCallback(() => (
|
||||
<div className="space-y-6">
|
||||
{/* Validation 에러 표시 */}
|
||||
@@ -237,58 +231,28 @@ export function WorkOrderEdit({ orderId }: WorkOrderEditProps) {
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* 기본 정보 (읽기 전용) */}
|
||||
<section className="bg-muted/30 border rounded-lg p-6">
|
||||
{/* 기본 정보 (기획서 4열 구성) */}
|
||||
<section className="bg-amber-50 border border-amber-200 rounded-lg p-6">
|
||||
<h3 className="font-semibold mb-4">기본 정보</h3>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>발주처</Label>
|
||||
<Input
|
||||
value={formData.client}
|
||||
disabled
|
||||
className="bg-muted"
|
||||
/>
|
||||
<div className="grid grid-cols-4 gap-x-6 gap-y-4">
|
||||
{/* 1행: 작업번호(읽기) | 수주일(읽기) | 공정구분(셀렉트) | 로트번호(읽기) */}
|
||||
<div className="space-y-1">
|
||||
<Label className="text-sm text-muted-foreground">작업번호</Label>
|
||||
<Input value={workOrder?.workOrderNo || '-'} disabled className="bg-muted" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>현장명</Label>
|
||||
<Input
|
||||
value={formData.projectName}
|
||||
onChange={(e) => setFormData({ ...formData, projectName: e.target.value })}
|
||||
className="bg-white"
|
||||
/>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-sm text-muted-foreground">수주일</Label>
|
||||
<Input value={workOrder?.salesOrderDate || '-'} disabled className="bg-muted" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>수주번호</Label>
|
||||
<Input
|
||||
value={formData.orderNo}
|
||||
disabled
|
||||
className="bg-muted"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>품목수</Label>
|
||||
<Input
|
||||
value={formData.itemCount || '-'}
|
||||
disabled
|
||||
className="bg-muted"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 작업지시 정보 */}
|
||||
<section className="bg-muted/30 border rounded-lg p-6">
|
||||
<h3 className="font-semibold mb-4">작업지시 정보</h3>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label>공정구분 *</Label>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-sm text-muted-foreground">공정구분 *</Label>
|
||||
<Select
|
||||
value={formData.processId?.toString() || ''}
|
||||
onValueChange={(value) => setFormData({ ...formData, processId: parseInt(value) })}
|
||||
disabled={isLoadingProcesses}
|
||||
>
|
||||
<SelectTrigger className="bg-white">
|
||||
<SelectValue placeholder={isLoadingProcesses ? '로딩 중...' : '공정을 선택하세요'} />
|
||||
<SelectValue placeholder={isLoadingProcesses ? '로딩 중...' : '공정 선택'} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{processOptions.map((process) => (
|
||||
@@ -298,13 +262,37 @@ export function WorkOrderEdit({ orderId }: WorkOrderEditProps) {
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
공정코드: {getSelectedProcessCode()}
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-sm text-muted-foreground">로트번호</Label>
|
||||
<Input value={formData.orderNo || '-'} disabled className="bg-muted" />
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>출고예정일 *</Label>
|
||||
{/* 2행: 수주처(읽기) | 현장명(입력) | 수주 담당자(읽기) | 담당자 연락처(읽기) */}
|
||||
<div className="space-y-1">
|
||||
<Label className="text-sm text-muted-foreground">수주처</Label>
|
||||
<Input value={formData.client} disabled className="bg-muted" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-sm text-muted-foreground">현장명</Label>
|
||||
<Input
|
||||
value={formData.projectName}
|
||||
onChange={(e) => setFormData({ ...formData, projectName: e.target.value })}
|
||||
className="bg-white"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-sm text-muted-foreground">수주 담당자</Label>
|
||||
<Input value="-" disabled className="bg-muted" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-sm text-muted-foreground">담당자 연락처</Label>
|
||||
<Input value="-" disabled className="bg-muted" />
|
||||
</div>
|
||||
|
||||
{/* 3행: 출고예정일(입력) | 틀수(읽기) | 우선순위(셀렉트) | 부서(읽기) */}
|
||||
<div className="space-y-1">
|
||||
<Label className="text-sm text-muted-foreground">출고예정일 *</Label>
|
||||
<Input
|
||||
type="date"
|
||||
value={formData.scheduledDate}
|
||||
@@ -312,9 +300,12 @@ export function WorkOrderEdit({ orderId }: WorkOrderEditProps) {
|
||||
className="bg-white"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>우선순위 (1=긴급, 9=낮음)</Label>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-sm text-muted-foreground">틀수</Label>
|
||||
<Input value={workOrder?.shutterCount ?? '-'} disabled className="bg-muted" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-sm text-muted-foreground">우선순위</Label>
|
||||
<Select
|
||||
value={formData.priority.toString()}
|
||||
onValueChange={(value) => setFormData({ ...formData, priority: parseInt(value) })}
|
||||
@@ -325,15 +316,20 @@ export function WorkOrderEdit({ orderId }: WorkOrderEditProps) {
|
||||
<SelectContent>
|
||||
{[1, 2, 3, 4, 5, 6, 7, 8, 9].map((n) => (
|
||||
<SelectItem key={n} value={n.toString()}>
|
||||
{n} {n === 5 ? '(일반)' : n === 1 ? '(긴급)' : ''}
|
||||
{n} {n <= 3 ? '(긴급)' : n <= 6 ? '(우선)' : '(일반)'}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-sm text-muted-foreground">부서</Label>
|
||||
<Input value={workOrder?.department || '-'} disabled className="bg-muted" />
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>담당자 (다중선택 가능)</Label>
|
||||
{/* 4행: 생산 담당자(선택) | 상태(읽기) | 비고(입력, colspan 2) */}
|
||||
<div className="space-y-1">
|
||||
<Label className="text-sm text-muted-foreground">생산 담당자</Label>
|
||||
<div
|
||||
onClick={() => setIsAssigneeModalOpen(true)}
|
||||
className="flex min-h-10 w-full cursor-pointer items-center rounded-md border border-input bg-white px-3 py-2 text-sm ring-offset-background hover:bg-accent/50"
|
||||
@@ -341,26 +337,28 @@ export function WorkOrderEdit({ orderId }: WorkOrderEditProps) {
|
||||
{assigneeNames.length > 0 ? (
|
||||
<span>{assigneeNames.join(', ')}</span>
|
||||
) : (
|
||||
<span className="text-muted-foreground">담당자를 선택하세요 (팀/개인)</span>
|
||||
<span className="text-muted-foreground">담당자 선택</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-sm text-muted-foreground">상태</Label>
|
||||
<Input value={workOrder ? (workOrder.status === 'waiting' ? '작업대기' : workOrder.status === 'in_progress' ? '작업중' : workOrder.status === 'completed' ? '작업완료' : workOrder.status) : '-'} disabled className="bg-muted" />
|
||||
</div>
|
||||
<div className="space-y-1 col-span-2">
|
||||
<Label className="text-sm text-muted-foreground">비고</Label>
|
||||
<Textarea
|
||||
value={formData.note}
|
||||
onChange={(e) => setFormData({ ...formData, note: e.target.value })}
|
||||
placeholder="특이사항이나 메모를 입력하세요"
|
||||
rows={2}
|
||||
className="bg-white"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 비고 */}
|
||||
<section className="bg-muted/30 border rounded-lg p-6">
|
||||
<h3 className="font-semibold mb-4">비고</h3>
|
||||
<Textarea
|
||||
value={formData.note}
|
||||
onChange={(e) => setFormData({ ...formData, note: e.target.value })}
|
||||
placeholder="특이사항이나 메모를 입력하세요"
|
||||
rows={4}
|
||||
className="bg-white"
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
), [formData, validationErrors, processOptions, isLoadingProcesses, assigneeNames, getSelectedProcessCode]);
|
||||
), [formData, validationErrors, processOptions, isLoadingProcesses, assigneeNames, workOrder]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 작업지시 목록 - UniversalListPage 마이그레이션
|
||||
* 작업지시 목록 - 공정 기반 탭 구조
|
||||
*
|
||||
* 기존 IntegratedListTemplateV2 → UniversalListPage config 기반으로 변환
|
||||
* - 서버 사이드 페이지네이션 (getWorkOrders API)
|
||||
* - 통계 카드 (getWorkOrderStats API)
|
||||
* - 탭 기반 상태 필터링
|
||||
* 기획서 기반 전면 개편:
|
||||
* - 탭: 공정 기반 3개 (스크린/슬랫/절곡) — 통계 카드 위에 배치
|
||||
* - 필터: 상태 + 우선순위
|
||||
* - 통계 카드 6개: 전체 작업 / 작업 대기 / 작업중 / 작업 완료 / 긴급 / 지연
|
||||
* - 컬럼: 작업번호/수주일/출고예정일/로트번호/수주처/현장명/틀수/상태/우선순위/부서/비고
|
||||
* - API: getProcessOptions로 공정 ID 매핑 후 processId로 필터링
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useMemo, useCallback } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Plus, FileText, Calendar, Users, CheckCircle2 } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { FileText, Clock, Loader, CheckCircle2, AlertTriangle, TimerOff } from 'lucide-react';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { TableCell, TableRow } from '@/components/ui/table';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
@@ -25,39 +26,104 @@ import {
|
||||
type StatCard,
|
||||
type ListParams,
|
||||
} from '@/components/templates/UniversalListPage';
|
||||
import type { FilterFieldConfig } from '@/components/molecules/MobileFilter';
|
||||
import { ListMobileCard, InfoField } from '@/components/organisms/MobileCard';
|
||||
import { toast } from 'sonner';
|
||||
import { getWorkOrders, getWorkOrderStats } from './actions';
|
||||
import { getWorkOrders, getWorkOrderStats, getProcessOptions } from './actions';
|
||||
import type { ProcessOption } from './actions';
|
||||
import {
|
||||
WORK_ORDER_STATUS_LABELS,
|
||||
WORK_ORDER_STATUS_COLORS,
|
||||
type WorkOrder,
|
||||
type WorkOrderStats,
|
||||
type WorkOrderStatus,
|
||||
} from './types';
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
|
||||
// 탭 필터 정의
|
||||
type TabFilter = 'all' | 'unassigned' | 'pending' | 'waiting' | 'in_progress' | 'completed';
|
||||
|
||||
// 페이지당 항목 수
|
||||
const ITEMS_PER_PAGE = 20;
|
||||
|
||||
// 작업자 표시 포맷 (홍길동 외 2명)
|
||||
function formatAssignees(item: WorkOrder): string {
|
||||
if (item.assignees && item.assignees.length > 0) {
|
||||
const primaryAssignee = item.assignees.find(a => a.isPrimary) || item.assignees[0];
|
||||
const otherCount = item.assignees.length - 1;
|
||||
if (otherCount > 0) {
|
||||
return `${primaryAssignee.name} 외 ${otherCount}명`;
|
||||
}
|
||||
return primaryAssignee.name;
|
||||
}
|
||||
return item.assignee || '-';
|
||||
}
|
||||
// 공정명 → 탭 value 매핑 (DB process_name 기준)
|
||||
const PROCESS_NAME_TO_TAB: Record<string, string> = {
|
||||
'스크린': 'screen',
|
||||
'슬랫': 'slat',
|
||||
'절곡': 'bending',
|
||||
};
|
||||
|
||||
// 공정코드 → 탭 value 매핑 (DB process_code 기준, 이름 매핑 실패 시 폴백)
|
||||
const PROCESS_CODE_TO_TAB: Record<string, string> = {
|
||||
'screen': 'screen',
|
||||
'slat': 'slat',
|
||||
'bending': 'bending',
|
||||
'SCREEN': 'screen',
|
||||
'SLAT': 'slat',
|
||||
'BENDING': 'bending',
|
||||
};
|
||||
|
||||
// 우선순위 뱃지 색상
|
||||
const PRIORITY_COLORS: Record<string, string> = {
|
||||
'긴급': 'bg-red-100 text-red-700',
|
||||
'우선': 'bg-orange-100 text-orange-700',
|
||||
'일반': 'bg-gray-100 text-gray-700',
|
||||
};
|
||||
|
||||
// 필터 설정: 상태 + 우선순위
|
||||
const filterConfig: FilterFieldConfig[] = [
|
||||
{
|
||||
key: 'status',
|
||||
label: '상태',
|
||||
type: 'single',
|
||||
options: [
|
||||
{ value: 'waiting', label: '작업대기' },
|
||||
{ value: 'in_progress', label: '진행중' },
|
||||
{ value: 'completed', label: '작업완료' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'priority',
|
||||
label: '우선순위',
|
||||
type: 'single',
|
||||
options: [
|
||||
{ value: 'urgent', label: '긴급' },
|
||||
{ value: 'priority', label: '우선' },
|
||||
{ value: 'normal', label: '일반' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export function WorkOrderList() {
|
||||
const router = useRouter();
|
||||
|
||||
// ===== 공정 ID 매핑 (getProcessOptions) =====
|
||||
const [processMap, setProcessMap] = useState<Record<string, number>>({});
|
||||
const [processMapLoaded, setProcessMapLoaded] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const loadProcessOptions = async () => {
|
||||
try {
|
||||
const result = await getProcessOptions();
|
||||
if (result.success && result.data) {
|
||||
const map: Record<string, number> = {};
|
||||
result.data.forEach((process: ProcessOption) => {
|
||||
// process_name 또는 process_code로 탭 매핑
|
||||
const tabKeyByName = PROCESS_NAME_TO_TAB[process.processName];
|
||||
const tabKeyByCode = PROCESS_CODE_TO_TAB[process.processCode];
|
||||
const tabKey = tabKeyByName || tabKeyByCode;
|
||||
if (tabKey) {
|
||||
map[tabKey] = process.id;
|
||||
}
|
||||
});
|
||||
setProcessMap(map);
|
||||
}
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[WorkOrderList] loadProcessOptions error:', error);
|
||||
} finally {
|
||||
setProcessMapLoaded(true);
|
||||
}
|
||||
};
|
||||
loadProcessOptions();
|
||||
}, []);
|
||||
|
||||
// ===== 통계 데이터 (외부 관리 - 별도 API) =====
|
||||
const [statsData, setStatsData] = useState<WorkOrderStats>({
|
||||
total: 0,
|
||||
@@ -97,46 +163,61 @@ export function WorkOrderList() {
|
||||
router.push('/ko/production/work-orders?mode=new');
|
||||
}, [router]);
|
||||
|
||||
// ===== 탭 옵션 (통계 데이터 기반) =====
|
||||
// ===== 탭 옵션 (공정 기반 3개) — 카운트는 API 응답으로 동적 업데이트 =====
|
||||
const [tabCounts, setTabCounts] = useState<Record<string, number>>({
|
||||
screen: 0,
|
||||
slat: 0,
|
||||
bending: 0,
|
||||
});
|
||||
|
||||
const tabs: TabOption[] = useMemo(
|
||||
() => [
|
||||
{ value: 'all', label: '전체', count: statsData.total },
|
||||
{ value: 'unassigned', label: '미배정', count: statsData.unassigned, color: 'gray' },
|
||||
{ value: 'pending', label: '승인대기', count: statsData.pending, color: 'orange' },
|
||||
{ value: 'waiting', label: '작업대기', count: statsData.waiting, color: 'yellow' },
|
||||
{ value: 'in_progress', label: '작업중', count: statsData.inProgress, color: 'blue' },
|
||||
{ value: 'completed', label: '작업완료', count: statsData.completed, color: 'green' },
|
||||
{ value: 'screen', label: '스크린 공정', count: tabCounts.screen },
|
||||
{ value: 'slat', label: '슬랫 공정', count: tabCounts.slat },
|
||||
{ value: 'bending', label: '절곡 공정', count: tabCounts.bending },
|
||||
],
|
||||
[statsData]
|
||||
[tabCounts]
|
||||
);
|
||||
|
||||
// ===== 통계 카드 =====
|
||||
// ===== 통계 카드 6개 (기획서 기반) =====
|
||||
const stats: StatCard[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
label: '전체',
|
||||
label: '전체 작업',
|
||||
value: statsData.total,
|
||||
icon: FileText,
|
||||
iconColor: 'text-gray-600',
|
||||
},
|
||||
{
|
||||
label: '미착수',
|
||||
label: '작업 대기',
|
||||
value: statsData.waiting + statsData.unassigned + statsData.pending,
|
||||
icon: Calendar,
|
||||
iconColor: 'text-orange-600',
|
||||
icon: Clock,
|
||||
iconColor: 'text-yellow-600',
|
||||
},
|
||||
{
|
||||
label: '작업중',
|
||||
value: statsData.inProgress,
|
||||
icon: Users,
|
||||
icon: Loader,
|
||||
iconColor: 'text-blue-600',
|
||||
},
|
||||
{
|
||||
label: '작업완료',
|
||||
label: '작업 완료',
|
||||
value: statsData.completed,
|
||||
icon: CheckCircle2,
|
||||
iconColor: 'text-green-600',
|
||||
},
|
||||
{
|
||||
label: '긴급',
|
||||
value: 0, // TODO: API에서 긴급 건수 제공 시 연동
|
||||
icon: AlertTriangle,
|
||||
iconColor: 'text-red-600',
|
||||
},
|
||||
{
|
||||
label: '지연',
|
||||
value: 0, // TODO: API에서 지연 건수 제공 시 연동
|
||||
icon: TimerOff,
|
||||
iconColor: 'text-orange-600',
|
||||
},
|
||||
],
|
||||
[statsData]
|
||||
);
|
||||
@@ -157,15 +238,45 @@ export function WorkOrderList() {
|
||||
actions: {
|
||||
getList: async (params?: ListParams) => {
|
||||
try {
|
||||
// 탭 → processId 매핑
|
||||
const tabValue = params?.tab || 'screen';
|
||||
const processId = processMap[tabValue];
|
||||
|
||||
// 해당 공정이 DB에 없으면 빈 목록 반환
|
||||
if (!processId) {
|
||||
return {
|
||||
success: true,
|
||||
data: [],
|
||||
totalCount: 0,
|
||||
totalPages: 0,
|
||||
};
|
||||
}
|
||||
|
||||
// 필터 값 추출
|
||||
const statusFilter = params?.filters?.status as string | undefined;
|
||||
const priorityFilter = params?.filters?.priority as string | undefined;
|
||||
|
||||
const result = await getWorkOrders({
|
||||
page: params?.page || 1,
|
||||
perPage: params?.pageSize || ITEMS_PER_PAGE,
|
||||
status: params?.tab === 'all' ? undefined : (params?.tab as TabFilter),
|
||||
processId,
|
||||
status: statusFilter && statusFilter !== 'all'
|
||||
? (statusFilter as WorkOrderStatus)
|
||||
: undefined,
|
||||
priority: priorityFilter && priorityFilter !== 'all'
|
||||
? priorityFilter
|
||||
: undefined,
|
||||
search: params?.search || undefined,
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
// 통계도 다시 로드 (탭 변경 시 최신 데이터 반영)
|
||||
// 현재 탭의 카운트 업데이트
|
||||
setTabCounts((prev) => ({
|
||||
...prev,
|
||||
[tabValue]: result.pagination.total,
|
||||
}));
|
||||
|
||||
// 통계도 다시 로드
|
||||
const statsResult = await getWorkOrderStats();
|
||||
if (statsResult.success && statsResult.data) {
|
||||
setStatsData(statsResult.data);
|
||||
@@ -186,21 +297,20 @@ export function WorkOrderList() {
|
||||
},
|
||||
},
|
||||
|
||||
// 테이블 컬럼
|
||||
// 테이블 컬럼 (기획서 기반 12개)
|
||||
columns: [
|
||||
{ key: 'no', label: '번호', className: 'w-[60px] text-center' },
|
||||
{ key: 'workOrderNo', label: '작업지시번호', className: 'min-w-[140px]' },
|
||||
{ key: 'processType', label: '공정', className: 'w-[80px]' },
|
||||
{ key: 'lotNo', label: '로트번호', className: 'min-w-[120px]' },
|
||||
{ key: 'orderDate', label: '지시일', className: 'w-[100px]' },
|
||||
{ key: 'isAssigned', label: '배정', className: 'w-[60px] text-center' },
|
||||
{ key: 'hasWork', label: '작업', className: 'w-[60px] text-center' },
|
||||
{ key: 'isStarted', label: '시작', className: 'w-[60px] text-center' },
|
||||
{ key: 'status', label: '작업상태', className: 'w-[100px]' },
|
||||
{ key: 'priority', label: '현장순위', className: 'w-[80px] text-center' },
|
||||
{ key: 'assignee', label: '작업자', className: 'w-[80px]' },
|
||||
{ key: 'projectName', label: '현장명', className: 'min-w-[150px]' },
|
||||
{ key: 'workOrderNo', label: '작업번호', className: 'min-w-[140px]' },
|
||||
{ key: 'salesOrderDate', label: '수주일', className: 'w-[100px]' },
|
||||
{ key: 'shipmentDate', label: '출고예정일', className: 'w-[110px]' },
|
||||
{ key: 'lotNo', label: '로트번호', className: 'min-w-[120px]' },
|
||||
{ key: 'client', label: '수주처', className: 'min-w-[120px]' },
|
||||
{ key: 'projectName', label: '현장명', className: 'min-w-[150px]' },
|
||||
{ key: 'shutterCount', label: '틀수', className: 'w-[70px] text-center' },
|
||||
{ key: 'status', label: '상태', className: 'w-[90px]' },
|
||||
{ key: 'priority', label: '우선순위', className: 'w-[80px]' },
|
||||
{ key: 'department', label: '부서', className: 'w-[90px]' },
|
||||
{ key: 'note', label: '비고', className: 'min-w-[120px]' },
|
||||
],
|
||||
|
||||
// 서버 사이드 페이지네이션
|
||||
@@ -208,7 +318,7 @@ export function WorkOrderList() {
|
||||
itemsPerPage: ITEMS_PER_PAGE,
|
||||
|
||||
// 검색
|
||||
searchPlaceholder: '작업지시번호, 발주처, 현장명 검색...',
|
||||
searchPlaceholder: '작업번호, 수주처, 현장명 검색...',
|
||||
searchFilter: (item: WorkOrder, search: string) => {
|
||||
const s = search.toLowerCase();
|
||||
return (
|
||||
@@ -220,20 +330,32 @@ export function WorkOrderList() {
|
||||
);
|
||||
},
|
||||
|
||||
// 탭 설정
|
||||
tabs,
|
||||
defaultTab: 'all',
|
||||
// 필터 설정 (상태 + 우선순위)
|
||||
filterConfig,
|
||||
initialFilters: {
|
||||
status: 'all',
|
||||
priority: 'all',
|
||||
},
|
||||
|
||||
// 통계 카드
|
||||
// 탭 설정 (공정 기반) — 통계 카드 위에 배치
|
||||
tabs,
|
||||
defaultTab: 'screen',
|
||||
tabsPosition: 'above-stats',
|
||||
|
||||
// 통계 카드 (6개)
|
||||
stats,
|
||||
|
||||
// 헤더 액션 (등록 버튼)
|
||||
headerActions: () => (
|
||||
<Button onClick={handleCreate}>
|
||||
<Plus className="w-4 h-4 mr-1.5" />
|
||||
등록
|
||||
</Button>
|
||||
),
|
||||
// 날짜 범위 선택기
|
||||
dateRangeSelector: {
|
||||
enabled: true,
|
||||
showPresets: true,
|
||||
},
|
||||
|
||||
// 등록 버튼 (발주에서 넘어오는 형태로 변경 예정)
|
||||
// createButton: {
|
||||
// label: '등록',
|
||||
// onClick: handleCreate,
|
||||
// },
|
||||
|
||||
// 테이블 행 렌더링
|
||||
renderTableRow: (
|
||||
@@ -256,23 +378,24 @@ export function WorkOrderList() {
|
||||
</TableCell>
|
||||
<TableCell className="text-center text-muted-foreground">{globalIndex}</TableCell>
|
||||
<TableCell className="font-medium">{item.workOrderNo}</TableCell>
|
||||
<TableCell>{item.processName}</TableCell>
|
||||
<TableCell>{item.salesOrderDate}</TableCell>
|
||||
<TableCell>{item.shipmentDate}</TableCell>
|
||||
<TableCell>{item.lotNo}</TableCell>
|
||||
<TableCell>{item.orderDate}</TableCell>
|
||||
<TableCell className="text-center">{item.isAssigned ? 'Y' : '-'}</TableCell>
|
||||
<TableCell className="text-center">
|
||||
{item.status !== 'unassigned' && item.status !== 'pending' ? 'Y' : '-'}
|
||||
</TableCell>
|
||||
<TableCell className="text-center">{item.isStarted ? 'Y' : '-'}</TableCell>
|
||||
<TableCell>{item.client}</TableCell>
|
||||
<TableCell className="max-w-[200px] truncate">{item.projectName}</TableCell>
|
||||
<TableCell className="text-center">{item.shutterCount ?? '-'}</TableCell>
|
||||
<TableCell>
|
||||
<Badge className={`${WORK_ORDER_STATUS_COLORS[item.status]} border-0`}>
|
||||
{WORK_ORDER_STATUS_LABELS[item.status]}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-center">{item.priority}</TableCell>
|
||||
<TableCell>{formatAssignees(item)}</TableCell>
|
||||
<TableCell className="max-w-[200px] truncate">{item.projectName}</TableCell>
|
||||
<TableCell>{item.shipmentDate}</TableCell>
|
||||
<TableCell>
|
||||
<Badge className={`${PRIORITY_COLORS[item.priorityLabel] || 'bg-gray-100 text-gray-700'} border-0`}>
|
||||
{item.priorityLabel}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>{item.department}</TableCell>
|
||||
<TableCell className="max-w-[150px] truncate">{item.note || '-'}</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
},
|
||||
@@ -309,21 +432,32 @@ export function WorkOrderList() {
|
||||
}
|
||||
infoGrid={
|
||||
<div className="grid grid-cols-2 gap-x-4 gap-y-3">
|
||||
<InfoField label="공정" value={item.processName} />
|
||||
<InfoField label="수주처" value={item.client} />
|
||||
<InfoField label="로트번호" value={item.lotNo} />
|
||||
<InfoField label="발주처" value={item.client} />
|
||||
<InfoField label="작업자" value={formatAssignees(item)} />
|
||||
<InfoField label="지시일" value={item.orderDate} />
|
||||
<InfoField label="수주일" value={item.salesOrderDate} />
|
||||
<InfoField label="출고예정일" value={item.shipmentDate} />
|
||||
<InfoField label="현장순위" value={item.priority} />
|
||||
<InfoField label="틀수" value={item.shutterCount ?? '-'} />
|
||||
<InfoField label="우선순위" value={item.priorityLabel} />
|
||||
<InfoField label="부서" value={item.department} />
|
||||
<InfoField label="비고" value={item.note || '-'} />
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
);
|
||||
},
|
||||
}),
|
||||
[tabs, stats, handleRowClick, handleCreate]
|
||||
[tabs, stats, processMap, handleRowClick]
|
||||
);
|
||||
|
||||
// processMap 로딩 완료 전에는 UniversalListPage를 마운트하지 않음
|
||||
// (초기 fetch에서 processId가 undefined로 전달되어 전체 데이터가 반환되는 문제 방지)
|
||||
if (!processMapLoaded) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<Loader className="h-8 w-8 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <UniversalListPage config={config} />;
|
||||
}
|
||||
|
||||
@@ -48,6 +48,8 @@ export async function getWorkOrders(params?: {
|
||||
perPage?: number;
|
||||
status?: WorkOrderStatus | 'all';
|
||||
processId?: number | 'all'; // 공정 ID (FK → processes.id)
|
||||
processType?: 'screen' | 'slat' | 'bending'; // 공정 타입 필터
|
||||
priority?: string; // 우선순위 필터 (urgent/priority/normal)
|
||||
search?: string;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
@@ -74,6 +76,12 @@ export async function getWorkOrders(params?: {
|
||||
if (params?.processId && params.processId !== 'all') {
|
||||
searchParams.set('process_id', String(params.processId));
|
||||
}
|
||||
if (params?.processType) {
|
||||
searchParams.set('process_type', params.processType);
|
||||
}
|
||||
if (params?.priority && params.priority !== 'all') {
|
||||
searchParams.set('priority', params.priority);
|
||||
}
|
||||
if (params?.search) searchParams.set('search', params.search);
|
||||
if (params?.startDate) searchParams.set('start_date', params.startDate);
|
||||
if (params?.endDate) searchParams.set('end_date', params.endDate);
|
||||
|
||||
@@ -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';
|
||||
@@ -110,6 +110,7 @@ export interface WorkOrderItem {
|
||||
floorCode: string; // 층/부호
|
||||
specification: string; // 규격
|
||||
quantity: number;
|
||||
unit: string; // 단위
|
||||
}
|
||||
|
||||
// 전개도 상세 (절곡용)
|
||||
@@ -180,6 +181,12 @@ export interface WorkOrder {
|
||||
|
||||
// 우선순위
|
||||
priority: number; // 1~9 (1=긴급, 9=낮음)
|
||||
priorityLabel: string; // 우선순위 라벨 (긴급/우선/일반)
|
||||
|
||||
// 수주 관련
|
||||
salesOrderDate: string; // 수주일
|
||||
shutterCount: number | null; // 틀수
|
||||
department: string; // 부서명
|
||||
|
||||
// 품목
|
||||
items: WorkOrderItem[];
|
||||
@@ -306,9 +313,12 @@ export interface WorkOrderApi {
|
||||
updated_at: string;
|
||||
deleted_at: string | null;
|
||||
// Relations
|
||||
priority: number | null;
|
||||
shutter_count: number | null;
|
||||
sales_order?: {
|
||||
id: number;
|
||||
order_no: string;
|
||||
order_date?: string;
|
||||
client?: { id: number; name: string };
|
||||
};
|
||||
process?: {
|
||||
@@ -370,6 +380,14 @@ export function transformApiToFrontend(api: WorkOrderApi): WorkOrder {
|
||||
return mapping[name] || 'screen';
|
||||
};
|
||||
|
||||
// 우선순위 매핑 (1~3: 긴급, 4~6: 우선, 7~9: 일반)
|
||||
const priorityValue = api.priority ?? 5;
|
||||
const getPriorityLabel = (p: number): string => {
|
||||
if (p <= 3) return '긴급';
|
||||
if (p <= 6) return '우선';
|
||||
return '일반';
|
||||
};
|
||||
|
||||
return {
|
||||
id: String(api.id),
|
||||
workOrderNo: api.work_order_no,
|
||||
@@ -397,7 +415,11 @@ export function transformApiToFrontend(api: WorkOrderApi): WorkOrder {
|
||||
shipmentDate: api.scheduled_date || '-',
|
||||
isAssigned: api.assignee_id !== null || assignees.length > 0,
|
||||
isStarted: ['in_progress', 'completed', 'shipped'].includes(api.status),
|
||||
priority: 5, // Default priority
|
||||
priority: priorityValue,
|
||||
priorityLabel: getPriorityLabel(priorityValue),
|
||||
salesOrderDate: api.sales_order?.order_date || api.created_at.split('T')[0],
|
||||
shutterCount: api.shutter_count ?? null,
|
||||
department: api.team?.name || '-',
|
||||
currentStep: getStatusStep(api.status),
|
||||
items: (api.items || []).map((item, idx) => ({
|
||||
id: String(item.id),
|
||||
@@ -407,6 +429,7 @@ export function transformApiToFrontend(api: WorkOrderApi): WorkOrder {
|
||||
floorCode: '-',
|
||||
specification: item.specification || '-',
|
||||
quantity: item.quantity,
|
||||
unit: item.unit || '-',
|
||||
})),
|
||||
bendingDetails: api.bending_detail ? transformBendingDetail(api.bending_detail) : undefined,
|
||||
issues: (api.issues || []).map(issue => ({
|
||||
|
||||
Reference in New Issue
Block a user