feat(WEB): 수입검사 관리 대폭 개선, 캘린더 DayTimeView 추가 및 출고 기능 보완
- 수입검사: InspectionCreate/Detail/List 대폭 개선, OrderSelectModal/문서 컴포넌트 신규 추가 - 수입검사: actions/types/mockData/inspectionConfig 전면 리팩토링 - QMS: InspectionModalV2/ImportInspectionDocument 개선 - 캘린더: DayTimeView 신규 추가, CalendarHeader/ScheduleCalendar/utils 확장 - 출고: ShipmentDetail/List/actions 개선, ShipmentOrderDocument/ShippingSlip 수정 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,348 +1,562 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 검사 등록 페이지
|
||||
* IntegratedDetailTemplate 마이그레이션 (2025-01-20)
|
||||
* API 연동 완료 (2025-12-26)
|
||||
* 제품검사 등록 페이지
|
||||
*
|
||||
* 기획서 기반 전면 재구축:
|
||||
* - 기본정보 입력
|
||||
* - 건축공사장, 자재유통업자, 공사시공자, 공사감리자 정보
|
||||
* - 검사 정보 (검사방문요청일, 기간, 검사자, 현장주소)
|
||||
* - 수주 설정 정보 (수주 선택 → 규격 비교 테이블)
|
||||
*/
|
||||
|
||||
import { useState, useCallback } from 'react';
|
||||
import { useState, useCallback, useMemo } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { ImageIcon } from 'lucide-react';
|
||||
import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate';
|
||||
import { qualityInspectionCreateConfig } from './inspectionConfig';
|
||||
import { Plus, Trash2 } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { QuantityInput } from '@/components/ui/quantity-input';
|
||||
import { NumberInput } from '@/components/ui/number-input';
|
||||
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate';
|
||||
import { qualityInspectionCreateConfig } from './inspectionConfig';
|
||||
import { toast } from 'sonner';
|
||||
import { createInspection } from './actions';
|
||||
import { isOrderSpecSame, calculateOrderSummary } from './mockData';
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
import { inspectionItemsTemplate, judgeMeasurement } from './mockData';
|
||||
import type { InspectionItem, QualityCheckItem, MeasurementItem } from './types';
|
||||
import { OrderSelectModal } from './OrderSelectModal';
|
||||
import type { InspectionFormData, OrderSettingItem, OrderSelectItem } from './types';
|
||||
import {
|
||||
emptyConstructionSite,
|
||||
emptyMaterialDistributor,
|
||||
emptyConstructor,
|
||||
emptySupervisor,
|
||||
emptyScheduleInfo,
|
||||
} from './mockData';
|
||||
|
||||
export function InspectionCreate() {
|
||||
const router = useRouter();
|
||||
|
||||
// 폼 상태
|
||||
const [formData, setFormData] = useState({
|
||||
lotNo: 'WO-251219-05', // 자동 (예시)
|
||||
itemName: '조인트바', // 자동 (예시)
|
||||
processName: '조립 공정', // 자동 (예시)
|
||||
quantity: 50,
|
||||
inspector: '',
|
||||
remarks: '',
|
||||
const [formData, setFormData] = useState<InspectionFormData>({
|
||||
qualityDocNumber: '',
|
||||
siteName: '',
|
||||
client: '',
|
||||
manager: '',
|
||||
managerContact: '',
|
||||
constructionSite: { ...emptyConstructionSite },
|
||||
materialDistributor: { ...emptyMaterialDistributor },
|
||||
constructorInfo: { ...emptyConstructor },
|
||||
supervisor: { ...emptySupervisor },
|
||||
scheduleInfo: { ...emptyScheduleInfo },
|
||||
orderItems: [],
|
||||
});
|
||||
|
||||
// 검사 항목 상태
|
||||
const [inspectionItems, setInspectionItems] = useState<InspectionItem[]>(
|
||||
inspectionItemsTemplate.map(item => ({ ...item }))
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [orderModalOpen, setOrderModalOpen] = useState(false);
|
||||
|
||||
// ===== 수주 선택 처리 =====
|
||||
const handleOrderSelect = useCallback((items: OrderSelectItem[]) => {
|
||||
const newOrderItems: OrderSettingItem[] = items.map((item) => ({
|
||||
id: item.id,
|
||||
orderNumber: item.orderNumber,
|
||||
floor: '',
|
||||
symbol: '',
|
||||
orderWidth: 0,
|
||||
orderHeight: 0,
|
||||
constructionWidth: 0,
|
||||
constructionHeight: 0,
|
||||
changeReason: '',
|
||||
}));
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
orderItems: [...prev.orderItems, ...newOrderItems],
|
||||
}));
|
||||
}, []);
|
||||
|
||||
// ===== 수주 항목 삭제 =====
|
||||
const handleRemoveOrderItem = useCallback((itemId: string) => {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
orderItems: prev.orderItems.filter((item) => item.id !== itemId),
|
||||
}));
|
||||
}, []);
|
||||
|
||||
// ===== 폼 필드 변경 헬퍼 =====
|
||||
const updateField = useCallback(<K extends keyof InspectionFormData>(
|
||||
key: K,
|
||||
value: InspectionFormData[K]
|
||||
) => {
|
||||
setFormData((prev) => ({ ...prev, [key]: value }));
|
||||
}, []);
|
||||
|
||||
const updateNested = useCallback((
|
||||
section: 'constructionSite' | 'materialDistributor' | 'constructorInfo' | 'supervisor' | 'scheduleInfo',
|
||||
field: string,
|
||||
value: string
|
||||
) => {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
[section]: {
|
||||
...(prev[section] as unknown as Record<string, unknown>),
|
||||
[field]: value,
|
||||
},
|
||||
}));
|
||||
}, []);
|
||||
|
||||
// ===== 수주 설정 요약 =====
|
||||
const orderSummary = useMemo(
|
||||
() => calculateOrderSummary(formData.orderItems),
|
||||
[formData.orderItems]
|
||||
);
|
||||
|
||||
// validation 에러 상태
|
||||
const [validationErrors, setValidationErrors] = useState<string[]>([]);
|
||||
|
||||
// 제출 상태
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
// 폼 입력 핸들러
|
||||
const handleInputChange = (field: string, value: string | number) => {
|
||||
setFormData(prev => ({ ...prev, [field]: value }));
|
||||
// 입력 시 에러 클리어
|
||||
if (validationErrors.length > 0) {
|
||||
setValidationErrors([]);
|
||||
}
|
||||
};
|
||||
|
||||
// 품질 검사 항목 결과 변경 (양호/불량)
|
||||
const handleQualityResultChange = useCallback((itemId: string, result: '양호' | '불량') => {
|
||||
setInspectionItems(prev => prev.map(item => {
|
||||
if (item.id === itemId && item.type === 'quality') {
|
||||
return {
|
||||
...item,
|
||||
result,
|
||||
judgment: result === '양호' ? '적합' : '부적합',
|
||||
} as QualityCheckItem;
|
||||
}
|
||||
return item;
|
||||
}));
|
||||
// 입력 시 에러 클리어
|
||||
setValidationErrors([]);
|
||||
}, []);
|
||||
|
||||
// 측정 항목 값 변경
|
||||
const handleMeasurementChange = useCallback((itemId: string, value: string) => {
|
||||
setInspectionItems(prev => prev.map(item => {
|
||||
if (item.id === itemId && item.type === 'measurement') {
|
||||
const measuredValue = parseFloat(value) || 0;
|
||||
const judgment = judgeMeasurement(item.spec, measuredValue);
|
||||
return {
|
||||
...item,
|
||||
measuredValue,
|
||||
judgment,
|
||||
} as MeasurementItem;
|
||||
}
|
||||
return item;
|
||||
}));
|
||||
// 입력 시 에러 클리어
|
||||
setValidationErrors([]);
|
||||
}, []);
|
||||
|
||||
// 취소
|
||||
// ===== 취소 =====
|
||||
const handleCancel = useCallback(() => {
|
||||
router.push('/quality/inspections');
|
||||
}, [router]);
|
||||
|
||||
// validation 체크
|
||||
const validateForm = (): boolean => {
|
||||
const errors: string[] = [];
|
||||
|
||||
// 필수 필드: 작업자
|
||||
if (!formData.inspector.trim()) {
|
||||
errors.push('작업자는 필수 입력 항목입니다.');
|
||||
// ===== 등록 제출 =====
|
||||
const handleSubmit = useCallback(async (): Promise<{ success: boolean; error?: string }> => {
|
||||
// 필수 필드 검증
|
||||
if (!formData.siteName.trim()) {
|
||||
toast.error('현장명은 필수 입력 항목입니다.');
|
||||
return { success: false, error: '현장명을 입력해주세요.' };
|
||||
}
|
||||
|
||||
// 검사 항목 validation
|
||||
inspectionItems.forEach((item, index) => {
|
||||
if (item.type === 'quality') {
|
||||
const qualityItem = item as QualityCheckItem;
|
||||
if (!qualityItem.result) {
|
||||
errors.push(`${index + 1}. ${item.name}: 결과를 선택해주세요.`);
|
||||
}
|
||||
} else if (item.type === 'measurement') {
|
||||
const measurementItem = item as MeasurementItem;
|
||||
if (measurementItem.measuredValue === undefined || measurementItem.measuredValue === null) {
|
||||
errors.push(`${index + 1}. ${item.name}: 측정값을 입력해주세요.`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
setValidationErrors(errors);
|
||||
return errors.length === 0;
|
||||
};
|
||||
|
||||
// 검사완료
|
||||
const handleSubmit = async () => {
|
||||
// validation 체크
|
||||
if (!validateForm()) {
|
||||
return;
|
||||
if (!formData.client.trim()) {
|
||||
toast.error('수주처는 필수 입력 항목입니다.');
|
||||
return { success: false, error: '수주처를 입력해주세요.' };
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
const result = await createInspection({
|
||||
inspectionType: 'PQC', // 기본값: 공정검사
|
||||
lotNo: formData.lotNo,
|
||||
itemName: formData.itemName,
|
||||
processName: formData.processName,
|
||||
quantity: formData.quantity,
|
||||
unit: 'EA', // 기본 단위
|
||||
remarks: formData.remarks || undefined,
|
||||
items: inspectionItems,
|
||||
});
|
||||
|
||||
const result = await createInspection(formData);
|
||||
if (result.success) {
|
||||
toast.success('검사가 등록되었습니다.');
|
||||
toast.success('제품검사가 등록되었습니다.');
|
||||
router.push('/quality/inspections');
|
||||
} else {
|
||||
toast.error(result.error || '검사 등록에 실패했습니다.');
|
||||
return { success: true };
|
||||
}
|
||||
return { success: false, error: result.error || '등록에 실패했습니다.' };
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[InspectionCreate] handleSubmit error:', error);
|
||||
toast.error('검사 등록 중 오류가 발생했습니다.');
|
||||
return { success: false, error: '등록 중 오류가 발생했습니다.' };
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
}, [formData, router]);
|
||||
|
||||
// ===== 폼 콘텐츠 렌더링 =====
|
||||
// ===== 수주 설정 테이블 =====
|
||||
const renderOrderTable = (items: OrderSettingItem[]) => (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-12 text-center">No.</TableHead>
|
||||
<TableHead>수주번호</TableHead>
|
||||
<TableHead>층수</TableHead>
|
||||
<TableHead>부호</TableHead>
|
||||
<TableHead className="text-center">수주 가로</TableHead>
|
||||
<TableHead className="text-center">수주 세로</TableHead>
|
||||
<TableHead className="text-center">시공 가로</TableHead>
|
||||
<TableHead className="text-center">시공 세로</TableHead>
|
||||
<TableHead className="text-center">일치</TableHead>
|
||||
<TableHead>변경사유</TableHead>
|
||||
<TableHead className="w-12 text-center">삭제</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{items.map((item, index) => {
|
||||
const isSame = isOrderSpecSame(item);
|
||||
return (
|
||||
<TableRow key={item.id}>
|
||||
<TableCell className="text-center">{index + 1}</TableCell>
|
||||
<TableCell>{item.orderNumber}</TableCell>
|
||||
<TableCell>{item.floor}</TableCell>
|
||||
<TableCell>{item.symbol}</TableCell>
|
||||
<TableCell className="text-center">{item.orderWidth}</TableCell>
|
||||
<TableCell className="text-center">{item.orderHeight}</TableCell>
|
||||
<TableCell className="text-center">{item.constructionWidth}</TableCell>
|
||||
<TableCell className="text-center">{item.constructionHeight}</TableCell>
|
||||
<TableCell className="text-center">
|
||||
{isSame ? (
|
||||
<Badge variant="outline" className="text-xs bg-green-50 text-green-700 border-green-200">일치</Badge>
|
||||
) : (
|
||||
<Badge variant="outline" className="text-xs bg-red-50 text-red-700 border-red-200">불일치</Badge>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>{item.changeReason || '-'}</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7 text-muted-foreground hover:text-red-600"
|
||||
type="button"
|
||||
onClick={() => handleRemoveOrderItem(item.id)}
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
{items.length === 0 && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={11} className="text-center text-muted-foreground py-8">
|
||||
수주를 선택해주세요.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
|
||||
// ===== 폼 렌더링 =====
|
||||
const renderFormContent = useCallback(() => (
|
||||
<div className="space-y-6">
|
||||
{/* Validation 에러 표시 */}
|
||||
{validationErrors.length > 0 && (
|
||||
<Alert className="bg-red-50 border-red-200">
|
||||
<AlertDescription className="text-red-900">
|
||||
<div className="flex items-start gap-2">
|
||||
<span className="text-lg">⚠️</span>
|
||||
<div className="flex-1">
|
||||
<strong className="block mb-2">
|
||||
입력 내용을 확인해주세요 ({validationErrors.length}개 오류)
|
||||
</strong>
|
||||
<ul className="space-y-1 text-sm">
|
||||
{validationErrors.map((error, index) => (
|
||||
<li key={index} className="flex items-start gap-1">
|
||||
<span>•</span>
|
||||
<span>{error}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
{/* 기본 정보 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">기본 정보</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>품질관리서 번호</Label>
|
||||
<Input
|
||||
value={formData.qualityDocNumber}
|
||||
onChange={(e) => updateField('qualityDocNumber', e.target.value)}
|
||||
placeholder="품질관리서 번호 입력"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>현장명 <span className="text-red-500">*</span></Label>
|
||||
<Input
|
||||
value={formData.siteName}
|
||||
onChange={(e) => updateField('siteName', e.target.value)}
|
||||
placeholder="현장명 입력"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>수주처 <span className="text-red-500">*</span></Label>
|
||||
<Input
|
||||
value={formData.client}
|
||||
onChange={(e) => updateField('client', e.target.value)}
|
||||
placeholder="수주처 입력"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>담당자</Label>
|
||||
<Input
|
||||
value={formData.manager}
|
||||
onChange={(e) => updateField('manager', e.target.value)}
|
||||
placeholder="담당자 입력"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>담당자 연락처</Label>
|
||||
<Input
|
||||
value={formData.managerContact}
|
||||
onChange={(e) => updateField('managerContact', e.target.value)}
|
||||
placeholder="담당자 연락처 입력"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 검사 개요 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">검사 개요</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-muted-foreground">LOT NO (자동)</Label>
|
||||
{/* 건축공사장 정보 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">건축공사장 정보</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>현장명</Label>
|
||||
<Input
|
||||
value={formData.constructionSite.siteName}
|
||||
onChange={(e) => updateNested('constructionSite', 'siteName', e.target.value)}
|
||||
placeholder="현장명 입력"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>대지위치</Label>
|
||||
<Input
|
||||
value={formData.constructionSite.landLocation}
|
||||
onChange={(e) => updateNested('constructionSite', 'landLocation', e.target.value)}
|
||||
placeholder="대지위치 입력"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>지번</Label>
|
||||
<Input
|
||||
value={formData.constructionSite.lotNumber}
|
||||
onChange={(e) => updateNested('constructionSite', 'lotNumber', e.target.value)}
|
||||
placeholder="지번 입력"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 자재유통업자 정보 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">자재유통업자 정보</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>회사명</Label>
|
||||
<Input
|
||||
value={formData.materialDistributor.companyName}
|
||||
onChange={(e) => updateNested('materialDistributor', 'companyName', e.target.value)}
|
||||
placeholder="회사명 입력"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>회사주소</Label>
|
||||
<Input
|
||||
value={formData.materialDistributor.companyAddress}
|
||||
onChange={(e) => updateNested('materialDistributor', 'companyAddress', e.target.value)}
|
||||
placeholder="회사주소 입력"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>대표자명</Label>
|
||||
<Input
|
||||
value={formData.materialDistributor.representativeName}
|
||||
onChange={(e) => updateNested('materialDistributor', 'representativeName', e.target.value)}
|
||||
placeholder="대표자명 입력"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>전화번호</Label>
|
||||
<Input
|
||||
value={formData.materialDistributor.phone}
|
||||
onChange={(e) => updateNested('materialDistributor', 'phone', e.target.value)}
|
||||
placeholder="전화번호 입력"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 공사시공자 정보 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">공사시공자 정보</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>회사명</Label>
|
||||
<Input
|
||||
value={formData.constructorInfo.companyName}
|
||||
onChange={(e) => updateNested('constructorInfo', 'companyName', e.target.value)}
|
||||
placeholder="회사명 입력"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>회사주소</Label>
|
||||
<Input
|
||||
value={formData.constructorInfo.companyAddress}
|
||||
onChange={(e) => updateNested('constructorInfo', 'companyAddress', e.target.value)}
|
||||
placeholder="회사주소 입력"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>성명</Label>
|
||||
<Input
|
||||
value={formData.constructorInfo.name}
|
||||
onChange={(e) => updateNested('constructorInfo', 'name', e.target.value)}
|
||||
placeholder="성명 입력"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>전화번호</Label>
|
||||
<Input
|
||||
value={formData.constructorInfo.phone}
|
||||
onChange={(e) => updateNested('constructorInfo', 'phone', e.target.value)}
|
||||
placeholder="전화번호 입력"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 공사감리자 정보 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">공사감리자 정보</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>사무소명</Label>
|
||||
<Input
|
||||
value={formData.supervisor.officeName}
|
||||
onChange={(e) => updateNested('supervisor', 'officeName', e.target.value)}
|
||||
placeholder="사무소명 입력"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>사무소주소</Label>
|
||||
<Input
|
||||
value={formData.supervisor.officeAddress}
|
||||
onChange={(e) => updateNested('supervisor', 'officeAddress', e.target.value)}
|
||||
placeholder="사무소주소 입력"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>성명</Label>
|
||||
<Input
|
||||
value={formData.supervisor.name}
|
||||
onChange={(e) => updateNested('supervisor', 'name', e.target.value)}
|
||||
placeholder="성명 입력"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>전화번호</Label>
|
||||
<Input
|
||||
value={formData.supervisor.phone}
|
||||
onChange={(e) => updateNested('supervisor', 'phone', e.target.value)}
|
||||
placeholder="전화번호 입력"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 검사 정보 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">검사 정보</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>검사방문요청일</Label>
|
||||
<Input
|
||||
type="date"
|
||||
value={formData.scheduleInfo.visitRequestDate}
|
||||
onChange={(e) => updateNested('scheduleInfo', 'visitRequestDate', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>검사시작일</Label>
|
||||
<Input
|
||||
type="date"
|
||||
value={formData.scheduleInfo.startDate}
|
||||
onChange={(e) => updateNested('scheduleInfo', 'startDate', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>검사종료일</Label>
|
||||
<Input
|
||||
type="date"
|
||||
value={formData.scheduleInfo.endDate}
|
||||
onChange={(e) => updateNested('scheduleInfo', 'endDate', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>검사자</Label>
|
||||
<Input
|
||||
value={formData.scheduleInfo.inspector}
|
||||
onChange={(e) => updateNested('scheduleInfo', 'inspector', e.target.value)}
|
||||
placeholder="검사자 입력"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* 현장 주소 */}
|
||||
<div className="mt-4 grid grid-cols-4 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>우편번호</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
value={formData.lotNo}
|
||||
disabled
|
||||
className="bg-muted"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-muted-foreground">품목명 (자동)</Label>
|
||||
<Input
|
||||
value={formData.itemName}
|
||||
disabled
|
||||
className="bg-muted"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-muted-foreground">공정명 (자동)</Label>
|
||||
<Input
|
||||
value={formData.processName}
|
||||
disabled
|
||||
className="bg-muted"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>수량</Label>
|
||||
<QuantityInput
|
||||
value={formData.quantity}
|
||||
onChange={(value) => handleInputChange('quantity', value ?? 0)}
|
||||
placeholder="수량 입력"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>작업자 *</Label>
|
||||
<Input
|
||||
value={formData.inspector}
|
||||
onChange={(e) => handleInputChange('inspector', e.target.value)}
|
||||
placeholder="작업자 입력"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2 md:col-span-3">
|
||||
<Label>특이사항</Label>
|
||||
<Input
|
||||
value={formData.remarks}
|
||||
onChange={(e) => handleInputChange('remarks', e.target.value)}
|
||||
placeholder="특이사항 입력"
|
||||
value={formData.scheduleInfo.sitePostalCode}
|
||||
onChange={(e) => updateNested('scheduleInfo', 'sitePostalCode', e.target.value)}
|
||||
className="w-28"
|
||||
/>
|
||||
<Button variant="outline" size="sm" type="button">
|
||||
우편번호 찾기
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 검사 기준 및 도해 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">검사 기준 및 도해</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center justify-center h-48 bg-muted rounded-lg border-2 border-dashed">
|
||||
<div className="text-center text-muted-foreground">
|
||||
<ImageIcon className="w-12 h-12 mx-auto mb-2 opacity-50" />
|
||||
<p>템플릿에서 설정한 조인트바 표준 도면이 표시됨</p>
|
||||
</div>
|
||||
<div className="space-y-2 col-span-2">
|
||||
<Label>주소</Label>
|
||||
<Input
|
||||
value={formData.scheduleInfo.siteAddress}
|
||||
onChange={(e) => updateNested('scheduleInfo', 'siteAddress', e.target.value)}
|
||||
placeholder="주소 입력"
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div className="space-y-2">
|
||||
<Label>상세주소</Label>
|
||||
<Input
|
||||
value={formData.scheduleInfo.siteAddressDetail}
|
||||
onChange={(e) => updateNested('scheduleInfo', 'siteAddressDetail', e.target.value)}
|
||||
placeholder="상세주소 입력"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 검사 데이터 입력 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">검사 데이터 입력</CardTitle>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
* 측정값을 입력하면 판정이 자동 처리됩니다.
|
||||
</p>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
{inspectionItems.map((item, index) => (
|
||||
<div key={item.id} className="p-4 border rounded-lg">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h4 className="font-medium">
|
||||
{index + 1}. {item.name}
|
||||
{item.type === 'measurement' && ` (${(item as MeasurementItem).unit})`}
|
||||
</h4>
|
||||
<span className={`text-sm font-medium ${
|
||||
item.judgment === '적합' ? 'text-green-600' :
|
||||
item.judgment === '부적합' ? 'text-red-600' :
|
||||
'text-muted-foreground'
|
||||
}`}>
|
||||
판정: {item.judgment || '-'}
|
||||
</span>
|
||||
</div>
|
||||
{/* 수주 설정 정보 */}
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<CardTitle className="text-base">수주 설정 정보</CardTitle>
|
||||
<Button variant="outline" size="sm" type="button" onClick={() => setOrderModalOpen(true)}>
|
||||
<Plus className="w-4 h-4 mr-1" />
|
||||
수주 선택
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 text-sm">
|
||||
<span>전체: <strong>{orderSummary.total}</strong>건</span>
|
||||
<span className="text-green-600">일치: <strong>{orderSummary.same}</strong>건</span>
|
||||
<span className="text-red-600">불일치: <strong>{orderSummary.changed}</strong>건</span>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="p-0">
|
||||
{renderOrderTable(formData.orderItems)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
), [formData, orderSummary, updateField, updateNested, handleRemoveOrderItem, orderModalOpen]);
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-muted-foreground">기준(Spec)</Label>
|
||||
<Input
|
||||
value={item.spec}
|
||||
disabled
|
||||
className="bg-muted"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{item.type === 'quality' ? (
|
||||
<div className="space-y-2">
|
||||
<Label>결과 입력 *</Label>
|
||||
<RadioGroup
|
||||
value={(item as QualityCheckItem).result || ''}
|
||||
onValueChange={(value) => handleQualityResultChange(item.id, value as '양호' | '불량')}
|
||||
className="flex items-center gap-4 h-10"
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="양호" id={`${item.id}-pass`} />
|
||||
<Label htmlFor={`${item.id}-pass`} className="cursor-pointer">양호</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="불량" id={`${item.id}-fail`} />
|
||||
<Label htmlFor={`${item.id}-fail`} className="cursor-pointer">불량</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
<Label>측정값 입력 ({(item as MeasurementItem).unit}) *</Label>
|
||||
<NumberInput
|
||||
step={0.1}
|
||||
allowDecimal
|
||||
value={(item as MeasurementItem).measuredValue ?? undefined}
|
||||
onChange={(value) => handleMeasurementChange(item.id, String(value ?? ''))}
|
||||
placeholder={`측정값 입력 (${(item as MeasurementItem).unit})`}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
), [formData, inspectionItems, validationErrors, handleInputChange, handleQualityResultChange, handleMeasurementChange]);
|
||||
// 이미 선택된 수주 ID 목록
|
||||
const excludeOrderIds = useMemo(
|
||||
() => formData.orderItems.map((item) => item.id),
|
||||
[formData.orderItems]
|
||||
);
|
||||
|
||||
return (
|
||||
<IntegratedDetailTemplate
|
||||
config={qualityInspectionCreateConfig}
|
||||
mode="create"
|
||||
isLoading={false}
|
||||
isSubmitting={isSubmitting}
|
||||
onBack={handleCancel}
|
||||
onCancel={handleCancel}
|
||||
onSubmit={handleSubmit}
|
||||
renderForm={renderFormContent}
|
||||
/>
|
||||
<>
|
||||
<IntegratedDetailTemplate
|
||||
config={qualityInspectionCreateConfig}
|
||||
mode="create"
|
||||
isLoading={false}
|
||||
isSubmitting={isSubmitting}
|
||||
onBack={handleCancel}
|
||||
onCancel={handleCancel}
|
||||
onSubmit={handleSubmit}
|
||||
renderForm={renderFormContent}
|
||||
/>
|
||||
|
||||
<OrderSelectModal
|
||||
open={orderModalOpen}
|
||||
onOpenChange={setOrderModalOpen}
|
||||
onSelect={handleOrderSelect}
|
||||
excludeIds={excludeOrderIds}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user