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:
유병철
2026-02-02 16:46:52 +09:00
parent 1a69324d59
commit ca6247286a
28 changed files with 4195 additions and 1776 deletions

View File

@@ -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}
/>
</>
);
}
}