diff --git a/src/app/[locale]/(protected)/quality/qms/components/InspectionModalV2.tsx b/src/app/[locale]/(protected)/quality/qms/components/InspectionModalV2.tsx index ed300c2f..d383ceb7 100644 --- a/src/app/[locale]/(protected)/quality/qms/components/InspectionModalV2.tsx +++ b/src/app/[locale]/(protected)/quality/qms/components/InspectionModalV2.tsx @@ -1,8 +1,10 @@ "use client"; -import React, { useState, useEffect } from 'react'; -import { AlertCircle, Loader2 } from 'lucide-react'; +import React, { useState, useEffect, useRef, useCallback } from 'react'; +import { AlertCircle, Loader2, Save } from 'lucide-react'; import { DocumentViewer } from '@/components/document-system'; +import { Button } from '@/components/ui/button'; +import { toast } from 'sonner'; import { Document, DocumentItem } from '../types'; import { MOCK_ORDER_DATA, MOCK_SHIPMENT_DETAIL } from '../mockData'; @@ -17,7 +19,7 @@ import { JointbarInspectionDocument, QualityDocumentUploader, } from './documents'; -import type { ImportInspectionTemplate } from './documents/ImportInspectionDocument'; +import type { ImportInspectionTemplate, ImportInspectionRef } from './documents/ImportInspectionDocument'; // 작업일지 + 중간검사 성적서 문서 컴포넌트 import (공정별 신규 버전) import { @@ -293,6 +295,10 @@ export const InspectionModalV2 = ({ const [isLoadingTemplate, setIsLoadingTemplate] = useState(false); const [templateError, setTemplateError] = useState(null); + // 수입검사 저장용 ref/상태 + const importDocRef = useRef(null); + const [isSaving, setIsSaving] = useState(false); + // 수입검사 템플릿 로드 (모달 열릴 때) useEffect(() => { if (isOpen && doc?.type === 'import' && itemName && specification) { @@ -385,6 +391,23 @@ export const InspectionModalV2 = ({ } }; + // 수입검사 저장 핸들러 + const handleImportSave = useCallback(async () => { + if (!importDocRef.current) return; + + const data = importDocRef.current.getInspectionData(); + setIsSaving(true); + try { + // TODO: 실제 저장 API 연동 + console.log('[InspectionModalV2] 수입검사 저장 데이터:', data); + toast.success('검사 데이터가 저장되었습니다.'); + } catch { + toast.error('저장 중 오류가 발생했습니다.'); + } finally { + setIsSaving(false); + } + }, []); + // 수입검사 문서 렌더링 (Lazy Loading) const renderImportInspectionDocument = () => { if (isLoadingTemplate) { @@ -396,7 +419,7 @@ export const InspectionModalV2 = ({ } // 템플릿이 로드되면 전달, 아니면 기본 템플릿 사용 - return ; + return ; }; // 문서 타입에 따른 컨텐츠 렌더링 @@ -433,6 +456,18 @@ export const InspectionModalV2 = ({ console.log('[InspectionModalV2] 다운로드 요청:', doc.type); }; + // 수입검사 저장 버튼 (toolbarExtra) + const importToolbarExtra = doc.type === 'import' ? ( + + ) : undefined; + return ( !open && onClose()} onDownload={handleDownload} + toolbarExtra={importToolbarExtra} > {renderDocumentContent()} diff --git a/src/app/[locale]/(protected)/quality/qms/components/documents/ImportInspectionDocument.tsx b/src/app/[locale]/(protected)/quality/qms/components/documents/ImportInspectionDocument.tsx index 31b6b569..0bea6af9 100644 --- a/src/app/[locale]/(protected)/quality/qms/components/documents/ImportInspectionDocument.tsx +++ b/src/app/[locale]/(protected)/quality/qms/components/documents/ImportInspectionDocument.tsx @@ -9,8 +9,7 @@ * - API 데이터 기반 동적 렌더링 */ -import React, { useState, useCallback, useMemo } from 'react'; -import { DocumentHeader, QualityApprovalTable } from '@/components/document-system'; +import React, { useState, useCallback, useMemo, forwardRef, useImperativeHandle } from 'react'; // ============================================ // 타입 정의 @@ -92,6 +91,11 @@ export interface ImportInspectionTemplate { notes?: string[]; } +/** ref를 통한 데이터 접근 인터페이스 */ +export interface ImportInspectionRef { + getInspectionData: () => unknown; +} + /** 컴포넌트 Props */ export interface ImportInspectionDocumentProps { template?: ImportInspectionTemplate; @@ -325,12 +329,12 @@ const isValueInRange = ( // 컴포넌트 // ============================================ -export const ImportInspectionDocument = ({ +export const ImportInspectionDocument = forwardRef(function ImportInspectionDocument({ template = MOCK_EGI_TEMPLATE, initialValues, onValuesChange, readOnly = false, -}: ImportInspectionDocumentProps) => { +}, ref) { // 검사 항목별 입력값 상태 const [values, setValues] = useState>(() => { const initial: Record = {}; @@ -416,6 +420,22 @@ export const ImportInspectionDocument = ({ return null; }, [values, template.inspectionItems]); + // ref를 통한 데이터 접근 + useImperativeHandle(ref, () => ({ + getInspectionData: () => ({ + templateId: template.templateId, + values: Object.values(values), + overallResult, + }), + }), [template.templateId, values, overallResult]); + + // 날짜 포맷 + const fullDate = new Date().toLocaleDateString('ko-KR', { + year: 'numeric', + month: 'long', + day: 'numeric', + }); + // OK/NG 선택 핸들러 const handleResultChange = useCallback((itemId: string, result: JudgmentResult) => { if (readOnly) return; @@ -599,18 +619,40 @@ export const ImportInspectionDocument = ({ return (
- {/* 문서 헤더 */} - - } - /> + {/* 문서 헤더 (중간검사 성적서 스타일) */} +
+
+

수입검사 성적서

+

+ 문서번호: {headerInfo.lotNo || template.templateId} | 작성일자: {fullDate} +

+
+ + {/* 결재란 */} + + + + + + + + + + + + + + + + + + + + + + +

작성승인승인승인
{headerInfo.approvers.writer || '-'}이름이름이름
부서명부서명부서명부서명
+
{/* 기본 정보 테이블 - 6컬럼 구조 */} @@ -807,4 +849,4 @@ export const ImportInspectionDocument = ({ ); -}; +}); diff --git a/src/app/[locale]/(protected)/quality/qms/mockData.ts b/src/app/[locale]/(protected)/quality/qms/mockData.ts index e209947f..16b6189b 100644 --- a/src/app/[locale]/(protected)/quality/qms/mockData.ts +++ b/src/app/[locale]/(protected)/quality/qms/mockData.ts @@ -107,6 +107,9 @@ export const MOCK_SHIPMENT_DETAIL: ShipmentDetail = { driverName: '최운전', driverContact: '010-5555-6666', remarks: '하차 시 주의 요망', + vehicleDispatches: [], + productGroups: [], + otherParts: [], }; // 품질관리서 목록 diff --git a/src/components/common/ScheduleCalendar/CalendarHeader.tsx b/src/components/common/ScheduleCalendar/CalendarHeader.tsx index e2827e48..882cfd93 100644 --- a/src/components/common/ScheduleCalendar/CalendarHeader.tsx +++ b/src/components/common/ScheduleCalendar/CalendarHeader.tsx @@ -4,7 +4,7 @@ import { ChevronLeft, ChevronRight } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { cn } from '@/components/ui/utils'; import type { CalendarHeaderProps, CalendarView } from './types'; -import { formatYearMonth } from './utils'; +import { formatYearMonth, formatYearMonthDay } from './utils'; /** * 달력 헤더 컴포넌트 @@ -27,6 +27,11 @@ export function CalendarHeader({ { value: 'month', label: '월' }, ]; + // 뷰에 따른 날짜 표시 형식 + const dateLabel = view === 'day-time' + ? formatYearMonthDay(currentDate) + : formatYearMonth(currentDate); + // 뷰 전환 버튼 렌더링 (재사용) const renderViewTabs = (className?: string) => (
@@ -71,7 +76,7 @@ export function CalendarHeader({ - {formatYearMonth(currentDate)} + {dateLabel} - - {formatYearMonth(currentDate)} + + {dateLabel} - -
- - - {detail && ( -
- {previewDocument === 'shipping' && } - {previewDocument === 'delivery' && } -
- )} - - + {/* 문서 미리보기 (DocumentViewer 통일 패턴) */} + setPreviewDocument(null)} + > + {detail && ( + <> + {previewDocument === 'shipping' && } + {previewDocument === 'delivery' && } + + )} + {/* 삭제 확인 다이얼로그 */} ('day-time'); const [shipmentData, setShipmentData] = useState([]); - // 데이터 로드 후 캘린더를 데이터 날짜로 이동 - useEffect(() => { - if (!calendarDateInitialized && shipmentData.length > 0) { - const firstDate = shipmentData[0].scheduledDate; - if (firstDate) { - setCalendarDate(new Date(firstDate)); - setCalendarDateInitialized(true); - } - } - }, [shipmentData, calendarDateInitialized]); - // 초기 통계 로드 useEffect(() => { const loadStats = async () => { @@ -414,23 +403,27 @@ export function ShipmentList() { ); }, - // 하단 캘린더 (시간축 주간 뷰) + // 하단 캘린더 (일/주 토글) afterTableContent: ( ), }), - [stats, startDate, endDate, scheduleEvents, calendarDate, handleRowClick, handleCreate, handleCalendarDateClick, handleCalendarEventClick] + [stats, startDate, endDate, scheduleEvents, calendarDate, scheduleView, handleRowClick, handleCreate, handleCalendarDateClick, handleCalendarEventClick] ); return ; diff --git a/src/components/outbound/ShipmentManagement/actions.ts b/src/components/outbound/ShipmentManagement/actions.ts index f8571369..81e65f4b 100644 --- a/src/components/outbound/ShipmentManagement/actions.ts +++ b/src/components/outbound/ShipmentManagement/actions.ts @@ -178,12 +178,12 @@ function transformApiToDetail(data: ShipmentApiData): ShipmentDetail { shipmentNo: data.shipment_no, lotNo: data.lot_no || '', scheduledDate: data.scheduled_date, - shipmentDate: (data as Record).shipment_date as string | undefined, + shipmentDate: (data as unknown as Record).shipment_date as string | undefined, status: data.status, priority: data.priority, deliveryMethod: data.delivery_method, - freightCost: (data as Record).freight_cost as FreightCostType | undefined, - freightCostLabel: (data as Record).freight_cost_label as string | undefined, + freightCost: (data as unknown as Record).freight_cost as FreightCostType | undefined, + freightCostLabel: (data as unknown as Record).freight_cost_label as string | undefined, depositConfirmed: data.deposit_confirmed, invoiceIssued: data.invoice_issued, customerGrade: data.customer_grade || '', @@ -197,11 +197,21 @@ function transformApiToDetail(data: ShipmentApiData): ShipmentDetail { deliveryAddress: data.order_info?.delivery_address || data.delivery_address || '', receiver: data.receiver, receiverContact: data.order_info?.contact || data.receiver_contact, - zipCode: (data as Record).zip_code as string | undefined, - address: (data as Record).address as string | undefined, - addressDetail: (data as Record).address_detail as string | undefined, - // 배차 정보 (다중 행) - API 준비 후 연동 - vehicleDispatches: [], + zipCode: (data as unknown as Record).zip_code as string | undefined, + address: (data as unknown as Record).address as string | undefined, + addressDetail: (data as unknown as Record).address_detail as string | undefined, + // 배차 정보 - 기존 단일 필드에서 구성 (다중 행 API 준비 전까지) + vehicleDispatches: data.vehicle_no || data.logistics_company || data.driver_contact + ? [{ + id: `vd-${data.id}`, + logisticsCompany: data.logistics_company || '-', + arrivalDateTime: data.confirmed_arrival || data.expected_arrival || '-', + tonnage: data.vehicle_tonnage || '-', + vehicleNo: data.vehicle_no || '-', + driverContact: data.driver_contact || '-', + remarks: '', + }] + : [], // 제품내용 (그룹핑) - 프론트엔드에서 그룹핑 처리 productGroups: [], otherParts: [], diff --git a/src/components/outbound/ShipmentManagement/documents/ShipmentOrderDocument.tsx b/src/components/outbound/ShipmentManagement/documents/ShipmentOrderDocument.tsx index 1fbe6697..449387a1 100644 --- a/src/components/outbound/ShipmentManagement/documents/ShipmentOrderDocument.tsx +++ b/src/components/outbound/ShipmentManagement/documents/ShipmentOrderDocument.tsx @@ -13,9 +13,10 @@ import { ConstructionApprovalTable } from '@/components/document-system'; interface ShipmentOrderDocumentProps { title: string; data: ShipmentDetail; + showDispatchInfo?: boolean; } -export function ShipmentOrderDocument({ title, data }: ShipmentOrderDocumentProps) { +export function ShipmentOrderDocument({ title, data, showDispatchInfo = false }: ShipmentOrderDocumentProps) { // 스크린 제품 필터링 (productGroups 기반) const screenProducts = data.productGroups.filter(g => g.productName?.includes('스크린') || @@ -188,6 +189,38 @@ export function ShipmentOrderDocument({ title, data }: ShipmentOrderDocumentProp + {/* 배차정보 (출고증에서만 표시) */} + {showDispatchInfo && (() => { + const dispatch = data.vehicleDispatches[0]; + return ( +
+
배차정보
+
+ + + + + + + + + + + + + + + + + + + + +
물류업체{dispatch?.logisticsCompany || data.logisticsCompany || '-'}입차일시{dispatch?.arrivalDateTime || '-'}
톤수{dispatch?.tonnage || data.vehicleTonnage || '-'}차량번호{dispatch?.vehicleNo || data.vehicleNo || '-'}기사연락처{dispatch?.driverContact || data.driverContact || '-'}
비고{dispatch?.remarks || '-'}
+
+ ); + })()} +

아래와 같이 주문하오니 품질 및 납기일을 준수하여 주시기 바랍니다.

{/* 1. 스크린 테이블 */} diff --git a/src/components/outbound/ShipmentManagement/documents/ShippingSlip.tsx b/src/components/outbound/ShipmentManagement/documents/ShippingSlip.tsx index 9ec95fd5..b25b7bcc 100644 --- a/src/components/outbound/ShipmentManagement/documents/ShippingSlip.tsx +++ b/src/components/outbound/ShipmentManagement/documents/ShippingSlip.tsx @@ -13,5 +13,5 @@ interface ShippingSlipProps { } export function ShippingSlip({ data }: ShippingSlipProps) { - return ; + return ; } diff --git a/src/components/quality/InspectionManagement/InspectionCreate.tsx b/src/components/quality/InspectionManagement/InspectionCreate.tsx index c618119d..c83a17d8 100644 --- a/src/components/quality/InspectionManagement/InspectionCreate.tsx +++ b/src/components/quality/InspectionManagement/InspectionCreate.tsx @@ -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({ + qualityDocNumber: '', + siteName: '', + client: '', + manager: '', + managerContact: '', + constructionSite: { ...emptyConstructionSite }, + materialDistributor: { ...emptyMaterialDistributor }, + constructorInfo: { ...emptyConstructor }, + supervisor: { ...emptySupervisor }, + scheduleInfo: { ...emptyScheduleInfo }, + orderItems: [], }); - // 검사 항목 상태 - const [inspectionItems, setInspectionItems] = useState( - 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(( + 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), + [field]: value, + }, + })); + }, []); + + // ===== 수주 설정 요약 ===== + const orderSummary = useMemo( + () => calculateOrderSummary(formData.orderItems), + [formData.orderItems] ); - // validation 에러 상태 - const [validationErrors, setValidationErrors] = useState([]); - - // 제출 상태 - 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[]) => ( + + + + No. + 수주번호 + 층수 + 부호 + 수주 가로 + 수주 세로 + 시공 가로 + 시공 세로 + 일치 + 변경사유 + 삭제 + + + + {items.map((item, index) => { + const isSame = isOrderSpecSame(item); + return ( + + {index + 1} + {item.orderNumber} + {item.floor} + {item.symbol} + {item.orderWidth} + {item.orderHeight} + {item.constructionWidth} + {item.constructionHeight} + + {isSame ? ( + 일치 + ) : ( + 불일치 + )} + + {item.changeReason || '-'} + + + + + ); + })} + {items.length === 0 && ( + + + 수주를 선택해주세요. + + + )} + +
+ ); + + // ===== 폼 렌더링 ===== const renderFormContent = useCallback(() => (
- {/* Validation 에러 표시 */} - {validationErrors.length > 0 && ( - - -
- ⚠️ -
- - 입력 내용을 확인해주세요 ({validationErrors.length}개 오류) - -
    - {validationErrors.map((error, index) => ( -
  • - - {error} -
  • - ))} -
-
-
-
-
- )} + {/* 기본 정보 */} + + + 기본 정보 + + +
+
+ + updateField('qualityDocNumber', e.target.value)} + placeholder="품질관리서 번호 입력" + /> +
+
+ + updateField('siteName', e.target.value)} + placeholder="현장명 입력" + /> +
+
+ + updateField('client', e.target.value)} + placeholder="수주처 입력" + /> +
+
+ + updateField('manager', e.target.value)} + placeholder="담당자 입력" + /> +
+
+ + updateField('managerContact', e.target.value)} + placeholder="담당자 연락처 입력" + /> +
+
+
+
- {/* 검사 개요 */} - - - 검사 개요 - - -
-
- + {/* 건축공사장 정보 */} + + + 건축공사장 정보 + + +
+
+ + updateNested('constructionSite', 'siteName', e.target.value)} + placeholder="현장명 입력" + /> +
+
+ + updateNested('constructionSite', 'landLocation', e.target.value)} + placeholder="대지위치 입력" + /> +
+
+ + updateNested('constructionSite', 'lotNumber', e.target.value)} + placeholder="지번 입력" + /> +
+
+
+
+ + {/* 자재유통업자 정보 */} + + + 자재유통업자 정보 + + +
+
+ + updateNested('materialDistributor', 'companyName', e.target.value)} + placeholder="회사명 입력" + /> +
+
+ + updateNested('materialDistributor', 'companyAddress', e.target.value)} + placeholder="회사주소 입력" + /> +
+
+ + updateNested('materialDistributor', 'representativeName', e.target.value)} + placeholder="대표자명 입력" + /> +
+
+ + updateNested('materialDistributor', 'phone', e.target.value)} + placeholder="전화번호 입력" + /> +
+
+
+
+ + {/* 공사시공자 정보 */} + + + 공사시공자 정보 + + +
+
+ + updateNested('constructorInfo', 'companyName', e.target.value)} + placeholder="회사명 입력" + /> +
+
+ + updateNested('constructorInfo', 'companyAddress', e.target.value)} + placeholder="회사주소 입력" + /> +
+
+ + updateNested('constructorInfo', 'name', e.target.value)} + placeholder="성명 입력" + /> +
+
+ + updateNested('constructorInfo', 'phone', e.target.value)} + placeholder="전화번호 입력" + /> +
+
+
+
+ + {/* 공사감리자 정보 */} + + + 공사감리자 정보 + + +
+
+ + updateNested('supervisor', 'officeName', e.target.value)} + placeholder="사무소명 입력" + /> +
+
+ + updateNested('supervisor', 'officeAddress', e.target.value)} + placeholder="사무소주소 입력" + /> +
+
+ + updateNested('supervisor', 'name', e.target.value)} + placeholder="성명 입력" + /> +
+
+ + updateNested('supervisor', 'phone', e.target.value)} + placeholder="전화번호 입력" + /> +
+
+
+
+ + {/* 검사 정보 */} + + + 검사 정보 + + +
+
+ + updateNested('scheduleInfo', 'visitRequestDate', e.target.value)} + /> +
+
+ + updateNested('scheduleInfo', 'startDate', e.target.value)} + /> +
+
+ + updateNested('scheduleInfo', 'endDate', e.target.value)} + /> +
+
+ + updateNested('scheduleInfo', 'inspector', e.target.value)} + placeholder="검사자 입력" + /> +
+
+ {/* 현장 주소 */} +
+
+ +
-
-
- - -
-
- - -
-
- - handleInputChange('quantity', value ?? 0)} - placeholder="수량 입력" - /> -
-
- - handleInputChange('inspector', e.target.value)} - placeholder="작업자 입력" - /> -
-
- - handleInputChange('remarks', e.target.value)} - placeholder="특이사항 입력" + value={formData.scheduleInfo.sitePostalCode} + onChange={(e) => updateNested('scheduleInfo', 'sitePostalCode', e.target.value)} + className="w-28" /> +
- - - - {/* 검사 기준 및 도해 */} - - - 검사 기준 및 도해 - - -
-
- -

템플릿에서 설정한 조인트바 표준 도면이 표시됨

-
+
+ + updateNested('scheduleInfo', 'siteAddress', e.target.value)} + placeholder="주소 입력" + />
- - +
+ + updateNested('scheduleInfo', 'siteAddressDetail', e.target.value)} + placeholder="상세주소 입력" + /> +
+
+
+
- {/* 검사 데이터 입력 */} - - - 검사 데이터 입력 -

- * 측정값을 입력하면 판정이 자동 처리됩니다. -

-
- - {inspectionItems.map((item, index) => ( -
-
-

- {index + 1}. {item.name} - {item.type === 'measurement' && ` (${(item as MeasurementItem).unit})`} -

- - 판정: {item.judgment || '-'} - -
+ {/* 수주 설정 정보 */} + + +
+ 수주 설정 정보 + +
+
+ 전체: {orderSummary.total} + 일치: {orderSummary.same} + 불일치: {orderSummary.changed} +
+
+ + {renderOrderTable(formData.orderItems)} + +
+
+ ), [formData, orderSummary, updateField, updateNested, handleRemoveOrderItem, orderModalOpen]); -
-
- - -
- - {item.type === 'quality' ? ( -
- - handleQualityResultChange(item.id, value as '양호' | '불량')} - className="flex items-center gap-4 h-10" - > -
- - -
-
- - -
-
-
- ) : ( -
- - handleMeasurementChange(item.id, String(value ?? ''))} - placeholder={`측정값 입력 (${(item as MeasurementItem).unit})`} - /> -
- )} -
-
- ))} -
-
-
- ), [formData, inspectionItems, validationErrors, handleInputChange, handleQualityResultChange, handleMeasurementChange]); + // 이미 선택된 수주 ID 목록 + const excludeOrderIds = useMemo( + () => formData.orderItems.map((item) => item.id), + [formData.orderItems] + ); return ( - + <> + + + + ); -} \ No newline at end of file +} diff --git a/src/components/quality/InspectionManagement/InspectionDetail.tsx b/src/components/quality/InspectionManagement/InspectionDetail.tsx index 112c560c..687b3d5e 100644 --- a/src/components/quality/InspectionManagement/InspectionDetail.tsx +++ b/src/components/quality/InspectionManagement/InspectionDetail.tsx @@ -1,23 +1,30 @@ 'use client'; /** - * 검사 상세/수정 페이지 - * API 연동 완료 (2025-12-26) - * IntegratedDetailTemplate 마이그레이션 완료 (2026-01-20) + * 제품검사 상세/수정 페이지 + * + * 기획서 기반 전면 재구축: + * - 기본정보, 건축공사장, 자재유통업자, 공사시공자, 공사감리자 정보 + * - 검사 정보 (검사방문요청일, 기간, 검사자, 현장주소) + * - 수주 설정 정보 (규격 비교 테이블, 요약) + * - 헤더 액션 (검사제품요청서 보기, 제품검사하기, 검사 완료) */ import { useState, useCallback, useEffect, useMemo } from 'react'; import { useRouter, useSearchParams } from 'next/navigation'; -import { Printer, Paperclip, Loader2 } from 'lucide-react'; +import { + FileText, + PlayCircle, + CheckCircle2, + Loader2, + Plus, + Trash2, +} from 'lucide-react'; import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; -import { Label } from '@/components/ui/label'; -import { Textarea } from '@/components/ui/textarea'; -import { NumberInput } from '@/components/ui/number-input'; -import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; import { Badge } from '@/components/ui/badge'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; -import { Alert, AlertDescription } from '@/components/ui/alert'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; import { Table, TableBody, @@ -26,239 +33,730 @@ import { TableHeader, TableRow, } from '@/components/ui/table'; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from '@/components/ui/alert-dialog'; import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate'; import { inspectionConfig } from './inspectionConfig'; import { ServerErrorPage } from '@/components/common/ServerErrorPage'; import { toast } from 'sonner'; -import { getInspectionById, updateInspection } from './actions'; +import { + getInspectionById, + updateInspection, + completeInspection, +} from './actions'; +import { + statusColorMap, + isOrderSpecSame, + calculateOrderSummary, + buildRequestDocumentData, + buildReportDocumentData, +} from './mockData'; +import { InspectionRequestModal } from './documents/InspectionRequestModal'; +import { InspectionReportModal } from './documents/InspectionReportModal'; import { isNextRedirectError } from '@/lib/utils/redirect-error'; -import { judgmentColorMap, judgeMeasurement } from './mockData'; -import type { Inspection, InspectionItem, QualityCheckItem, MeasurementItem } from './types'; +import { OrderSelectModal } from './OrderSelectModal'; +import type { + ProductInspection, + InspectionFormData, + OrderSettingItem, + OrderSelectItem, +} from './types'; interface InspectionDetailProps { id: string; } +// 폼 초기값 +const EMPTY_FORM: InspectionFormData = { + qualityDocNumber: '', + siteName: '', + client: '', + manager: '', + managerContact: '', + constructionSite: { siteName: '', landLocation: '', lotNumber: '' }, + materialDistributor: { companyName: '', companyAddress: '', representativeName: '', phone: '' }, + constructorInfo: { companyName: '', companyAddress: '', name: '', phone: '' }, + supervisor: { officeName: '', officeAddress: '', name: '', phone: '' }, + scheduleInfo: { + visitRequestDate: '', + startDate: '', + endDate: '', + inspector: '', + sitePostalCode: '', + siteAddress: '', + siteAddressDetail: '', + }, + orderItems: [], +}; + export function InspectionDetail({ id }: InspectionDetailProps) { const router = useRouter(); const searchParams = useSearchParams(); const isEditMode = searchParams.get('mode') === 'edit'; - // 검사 데이터 상태 - const [inspection, setInspection] = useState(null); + // 상세 데이터 + const [inspection, setInspection] = useState(null); const [isLoading, setIsLoading] = useState(true); - // 수정 폼 상태 - const [editReason, setEditReason] = useState(''); - const [inspectionItems, setInspectionItems] = useState([]); + // 수정 폼 데이터 + const [formData, setFormData] = useState(EMPTY_FORM); + + // 검사 완료 다이얼로그 + const [showCompleteDialog, setShowCompleteDialog] = useState(false); + const [isCompleting, setIsCompleting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); - // validation 에러 상태 - const [validationErrors, setValidationErrors] = useState([]); + // 수주 선택 모달 + const [orderModalOpen, setOrderModalOpen] = useState(false); - // API로 검사 데이터 로드 + // 문서 모달 + const [requestDocOpen, setRequestDocOpen] = useState(false); + const [reportDocOpen, setReportDocOpen] = useState(false); + + // ===== API 데이터 로드 ===== const loadInspection = useCallback(async () => { setIsLoading(true); try { const result = await getInspectionById(id); if (result.success && result.data) { setInspection(result.data); - setInspectionItems(result.data.items || []); + // Edit 모드용 폼 데이터 초기화 + setFormData({ + qualityDocNumber: result.data.qualityDocNumber, + siteName: result.data.siteName, + client: result.data.client, + manager: result.data.manager, + managerContact: result.data.managerContact, + constructionSite: { ...result.data.constructionSite }, + materialDistributor: { ...result.data.materialDistributor }, + constructorInfo: { ...result.data.constructorInfo }, + supervisor: { ...result.data.supervisor }, + scheduleInfo: { ...result.data.scheduleInfo }, + orderItems: result.data.orderItems.map((item) => ({ ...item })), + }); } else { - toast.error(result.error || '검사 데이터를 불러오는데 실패했습니다.'); + toast.error(result.error || '제품검사 데이터를 불러오는데 실패했습니다.'); } } catch (error) { if (isNextRedirectError(error)) throw error; console.error('[InspectionDetail] loadInspection error:', error); - toast.error('검사 데이터 로드 중 오류가 발생했습니다.'); + toast.error('데이터 로드 중 오류가 발생했습니다.'); } finally { setIsLoading(false); } }, [id]); - // 컴포넌트 마운트 시 데이터 로드 useEffect(() => { loadInspection(); }, [loadInspection]); - // 품질 검사 항목 결과 변경 (양호/불량) - 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 handleBack = () => { - router.push('/quality/inspections'); - }; - - // 수정 모드 진입 - const handleEditMode = () => { + // ===== 네비게이션 ===== + const handleEdit = useCallback(() => { router.push(`/quality/inspections/${id}?mode=edit`); - }; + }, [id, router]); - // 수정 취소 - const handleCancelEdit = () => { - router.push(`/quality/inspections/${id}?mode=view`); - }; - - // validation 체크 - const validateForm = (): boolean => { - const errors: string[] = []; - - // 필수 필드: 수정 사유 - if (!editReason.trim()) { - errors.push('수정 사유는 필수 입력 항목입니다.'); - } - - // 검사 항목 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 handleSubmitEdit = async () => { - // validation 체크 - if (!validateForm()) { - return; - } - - setIsSubmitting(true); + // ===== 검사 완료 처리 ===== + const handleComplete = useCallback(async () => { + setIsCompleting(true); try { - const result = await updateInspection(id, { - items: inspectionItems, - remarks: editReason, - }); - + const result = await completeInspection(id); if (result.success) { - toast.success('검사가 수정되었습니다.'); - router.push(`/quality/inspections/${id}?mode=view`); + toast.success('검사가 완료 처리되었습니다.'); + setShowCompleteDialog(false); + loadInspection(); } else { - toast.error(result.error || '검사 수정에 실패했습니다.'); + toast.error(result.error || '검사 완료 처리에 실패했습니다.'); } } catch (error) { if (isNextRedirectError(error)) throw error; - console.error('[InspectionDetail] handleSubmitEdit error:', error); - toast.error('검사 수정 중 오류가 발생했습니다.'); + toast.error('검사 완료 처리 중 오류가 발생했습니다.'); } finally { - setIsSubmitting(false); + setIsCompleting(false); } - }; + }, [id, loadInspection]); - // 수정 사유 변경 핸들러 - const handleEditReasonChange = (value: string) => { - setEditReason(value); - // 입력 시 에러 클리어 - if (validationErrors.length > 0) { - setValidationErrors([]); - } - }; - - // 성적서 출력 - const handlePrintReport = () => { - // TODO: 성적서 출력 기능 - console.log('Print Report'); - }; - - // 저장 핸들러 (IntegratedDetailTemplate용) + // ===== 수정 제출 ===== const handleSubmit = useCallback(async (): Promise<{ success: boolean; error?: string }> => { - // validation 체크 - if (!validateForm()) { - return { success: false, error: '입력 내용을 확인해주세요.' }; - } - + setIsSubmitting(true); try { - const result = await updateInspection(id, { - items: inspectionItems, - remarks: editReason, - }); - + const result = await updateInspection(id, formData); if (result.success) { - toast.success('검사가 수정되었습니다.'); + toast.success('제품검사가 수정되었습니다.'); router.push(`/quality/inspections/${id}?mode=view`); return { success: true }; } - return { success: false, error: result.error || '검사 수정에 실패했습니다.' }; + return { success: false, error: result.error || '수정에 실패했습니다.' }; } catch (error) { if (isNextRedirectError(error)) throw error; - return { success: false, error: '검사 수정 중 오류가 발생했습니다.' }; + return { success: false, error: '수정 중 오류가 발생했습니다.' }; + } finally { + setIsSubmitting(false); } - }, [id, inspectionItems, editReason, router, validateForm]); + }, [id, formData, router]); - // 모드 결정 - const mode = isEditMode ? 'edit' : 'view'; + // ===== 폼 필드 변경 헬퍼 ===== + const updateField = useCallback(( + key: K, + value: InspectionFormData[K] + ) => { + setFormData((prev) => ({ ...prev, [key]: value })); + }, []); - // 동적 config (모드에 따른 타이틀 변경) - // IntegratedDetailTemplate: edit 모드에서 자동으로 "{title} 수정" 붙음 - const dynamicConfig = useMemo(() => { - if (isEditMode) { - return { - ...inspectionConfig, - title: '검사', - }; - } - return inspectionConfig; - }, [isEditMode]); + const updateNested = useCallback(( + section: 'constructionSite' | 'materialDistributor' | 'constructorInfo' | 'supervisor' | 'scheduleInfo', + field: string, + value: string + ) => { + setFormData((prev) => ({ + ...prev, + [section]: { + ...(prev[section] as unknown as Record), + [field]: value, + }, + })); + }, []); + + // ===== 수주 선택/삭제 처리 ===== + 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 excludeOrderIds = useMemo( + () => formData.orderItems.map((item) => item.id), + [formData.orderItems] + ); + + // ===== 수주 설정 요약 ===== + const orderSummary = useMemo(() => { + const items = isEditMode ? formData.orderItems : (inspection?.orderItems || []); + return calculateOrderSummary(items); + }, [isEditMode, formData.orderItems, inspection?.orderItems]); + + // ===== 정보 필드 렌더링 헬퍼 ===== + const renderInfoField = (label: string, value: React.ReactNode) => ( +
+
{label}
+
{value || '-'}
+
+ ); + + // ===== 수주 설정 테이블 ===== + const renderOrderTable = (items: OrderSettingItem[]) => ( + + + + No. + 수주번호 + 층수 + 부호 + 수주 가로 + 수주 세로 + 시공 가로 + 시공 세로 + 일치 + 변경사유 + + + + {items.map((item, index) => { + const isSame = isOrderSpecSame(item); + return ( + + {index + 1} + {item.orderNumber} + {item.floor} + {item.symbol} + {item.orderWidth} + {item.orderHeight} + {item.constructionWidth} + {item.constructionHeight} + + {isSame ? ( + 일치 + ) : ( + 불일치 + )} + + {item.changeReason || '-'} + + ); + })} + {items.length === 0 && ( + + + 수주 설정 정보가 없습니다. + + + )} + +
+ ); + + // ===== 수주 설정 테이블 (편집용 - 삭제 버튼 포함) ===== + const renderEditOrderTable = (items: OrderSettingItem[]) => ( + + + + No. + 수주번호 + 층수 + 부호 + 수주 가로 + 수주 세로 + 시공 가로 + 시공 세로 + 일치 + 변경사유 + 삭제 + + + + {items.map((item, index) => { + const isSame = isOrderSpecSame(item); + return ( + + {index + 1} + {item.orderNumber} + {item.floor} + {item.symbol} + {item.orderWidth} + {item.orderHeight} + {item.constructionWidth} + {item.constructionHeight} + + {isSame ? ( + 일치 + ) : ( + 불일치 + )} + + {item.changeReason || '-'} + + + + + ); + })} + {items.length === 0 && ( + + + 수주를 선택해주세요. + + + )} + +
+ ); + + // ===== 헤더 액션 버튼 (view 모드) ===== + const headerActions = useMemo(() => { + if (isEditMode || !inspection) return null; - // 커스텀 헤더 액션 (view 모드에서 성적서 버튼) - const customHeaderActions = useMemo(() => { - if (isEditMode) return null; return ( - +
+ + + {inspection.status !== '완료' && ( + + )} +
); - }, [isEditMode]); + }, [isEditMode, inspection]); - // View 모드 폼 내용 렌더링 - const renderViewContent = () => { + // ===== View 모드 렌더링 ===== + const renderViewContent = useCallback(() => { if (!inspection) return null; return (
+ {/* 기본 정보 */} + + + 기본 정보 + + +
+ {renderInfoField('품질관리서 번호', inspection.qualityDocNumber)} + {renderInfoField('현장명', inspection.siteName)} + {renderInfoField('수주처', inspection.client)} + {renderInfoField('접수일', inspection.receptionDate)} + {renderInfoField('담당자', inspection.manager)} + {renderInfoField('담당자 연락처', inspection.managerContact)} + {renderInfoField( + '상태', + + {inspection.status} + + )} + {renderInfoField('작성자', inspection.author)} +
+
+
+ + {/* 건축공사장 정보 */} + + + 건축공사장 정보 + + +
+ {renderInfoField('현장명', inspection.constructionSite.siteName)} + {renderInfoField('대지위치', inspection.constructionSite.landLocation)} + {renderInfoField('지번', inspection.constructionSite.lotNumber)} +
+
+
+ + {/* 자재유통업자 정보 */} + + + 자재유통업자 정보 + + +
+ {renderInfoField('회사명', inspection.materialDistributor.companyName)} + {renderInfoField('회사주소', inspection.materialDistributor.companyAddress)} + {renderInfoField('대표자명', inspection.materialDistributor.representativeName)} + {renderInfoField('전화번호', inspection.materialDistributor.phone)} +
+
+
+ + {/* 공사시공자 정보 */} + + + 공사시공자 정보 + + +
+ {renderInfoField('회사명', inspection.constructorInfo.companyName)} + {renderInfoField('회사주소', inspection.constructorInfo.companyAddress)} + {renderInfoField('성명', inspection.constructorInfo.name)} + {renderInfoField('전화번호', inspection.constructorInfo.phone)} +
+
+
+ + {/* 공사감리자 정보 */} + + + 공사감리자 정보 + + +
+ {renderInfoField('사무소명', inspection.supervisor.officeName)} + {renderInfoField('사무소주소', inspection.supervisor.officeAddress)} + {renderInfoField('성명', inspection.supervisor.name)} + {renderInfoField('전화번호', inspection.supervisor.phone)} +
+
+
+ + {/* 검사 정보 */} + + + 검사 정보 + + +
+ {renderInfoField('검사방문요청일', inspection.scheduleInfo.visitRequestDate)} + {renderInfoField('검사시작일', inspection.scheduleInfo.startDate)} + {renderInfoField('검사종료일', inspection.scheduleInfo.endDate)} + {renderInfoField('검사자', inspection.scheduleInfo.inspector)} +
+
+
현장 주소
+
+ {inspection.scheduleInfo.sitePostalCode && ( + [{inspection.scheduleInfo.sitePostalCode}] + )} + {inspection.scheduleInfo.siteAddress || '-'} + {inspection.scheduleInfo.siteAddressDetail && ( + {inspection.scheduleInfo.siteAddressDetail} + )} +
+
+
+
+ + {/* 수주 설정 정보 */} + + + 수주 설정 정보 +
+ 전체: {orderSummary.total} + 일치: {orderSummary.same} + 불일치: {orderSummary.changed} +
+
+ + {renderOrderTable(inspection.orderItems)} + +
+
+ ); + }, [inspection, orderSummary]); + + // ===== Edit 모드 폼 렌더링 ===== + const renderFormContent = useCallback(() => { + if (!inspection) return null; + + return ( +
+ {/* 기본 정보 */} + + + 기본 정보 + + +
+
+ + +
+
+ + updateField('siteName', e.target.value)} + /> +
+
+ + updateField('client', e.target.value)} + /> +
+
+ + +
+
+ + updateField('manager', e.target.value)} + /> +
+
+ + updateField('managerContact', e.target.value)} + /> +
+
+ + +
+
+ + +
+
+
+
+ + {/* 건축공사장 정보 */} + + + 건축공사장 정보 + + +
+
+ + updateNested('constructionSite', 'siteName', e.target.value)} + /> +
+
+ + updateNested('constructionSite', 'landLocation', e.target.value)} + /> +
+
+ + updateNested('constructionSite', 'lotNumber', e.target.value)} + /> +
+
+
+
+ + {/* 자재유통업자 정보 */} + + + 자재유통업자 정보 + + +
+
+ + updateNested('materialDistributor', 'companyName', e.target.value)} + /> +
+
+ + updateNested('materialDistributor', 'companyAddress', e.target.value)} + /> +
+
+ + updateNested('materialDistributor', 'representativeName', e.target.value)} + /> +
+
+ + updateNested('materialDistributor', 'phone', e.target.value)} + /> +
+
+
+
+ + {/* 공사시공자 정보 */} + + + 공사시공자 정보 + + +
+
+ + updateNested('constructorInfo', 'companyName', e.target.value)} + /> +
+
+ + updateNested('constructorInfo', 'companyAddress', e.target.value)} + /> +
+
+ + updateNested('constructorInfo', 'name', e.target.value)} + /> +
+
+ + updateNested('constructorInfo', 'phone', e.target.value)} + /> +
+
+
+
+ + {/* 공사감리자 정보 */} + + + 공사감리자 정보 + + +
+
+ + updateNested('supervisor', 'officeName', e.target.value)} + /> +
+
+ + updateNested('supervisor', 'officeAddress', e.target.value)} + /> +
+
+ + updateNested('supervisor', 'name', e.target.value)} + /> +
+
+ + updateNested('supervisor', 'phone', e.target.value)} + /> +
+
+
+
+ {/* 검사 정보 */} @@ -266,280 +764,111 @@ export function InspectionDetail({ id }: InspectionDetailProps) {
-
- -

{inspection.inspectionNo}

+
+ + updateNested('scheduleInfo', 'visitRequestDate', e.target.value)} + />
-
- -

{inspection.inspectionType}

+
+ + updateNested('scheduleInfo', 'startDate', e.target.value)} + />
-
- -

{inspection.inspectionDate || '-'}

+
+ + updateNested('scheduleInfo', 'endDate', e.target.value)} + />
-
- -

- {inspection.result && ( - - {inspection.result} - - )} - {!inspection.result && '-'} -

+
+ + updateNested('scheduleInfo', 'inspector', e.target.value)} + />
-
- -

{inspection.itemName}

+
+ {/* 현장 주소 */} +
+
+ +
+ updateNested('scheduleInfo', 'sitePostalCode', e.target.value)} + className="w-28" + /> + +
-
- -

{inspection.lotNo}

+
+ + updateNested('scheduleInfo', 'siteAddress', e.target.value)} + />
-
- -

{inspection.processName}

-
-
- -

{inspection.inspector || '-'}

+
+ + updateNested('scheduleInfo', 'siteAddressDetail', e.target.value)} + />
- {/* 검사 결과 데이터 */} + {/* 수주 설정 정보 */} - - 검사 결과 데이터 - - - - - - 항목명 - 기준(Spec) - 측정값/결과 - 판정 - - - - {inspection.items.map((item) => ( - - {item.name} - {item.spec} - - {item.type === 'quality' - ? (item as QualityCheckItem).result || '-' - : `${(item as MeasurementItem).measuredValue || '-'} ${(item as MeasurementItem).unit}` - } - - - - {item.judgment || '-'} - - - - ))} - {inspection.items.length === 0 && ( - - - 검사 데이터가 없습니다. - - - )} - -
-
-
- - {/* 종합 의견 */} - - - 종합 의견 - - -

{inspection.opinion || '의견이 없습니다.'}

-
-
- - {/* 첨부 파일 */} - - - 첨부 파일 - - - {inspection.attachments && inspection.attachments.length > 0 ? ( -
- {inspection.attachments.map((file) => ( - - ))} -
- ) : ( -

첨부 파일이 없습니다.

- )} -
-
-
- ); - }; - - // Edit 모드 폼 내용 렌더링 - const renderFormContent = () => { - if (!inspection) return null; - - return ( -
- {/* Validation 에러 표시 */} - {validationErrors.length > 0 && ( - - -
- ⚠️ -
- - 입력 내용을 확인해주세요 ({validationErrors.length}개 오류) - -
    - {validationErrors.map((error, index) => ( -
  • - - {error} -
  • - ))} -
-
-
-
-
- )} - - {/* 검사 개요 (수정 불가) */} - - - 검사 개요 (수정 불가) - - -
-
- - -
-
- - -
-
- - -
-
- - -
+ +
+ 수주 설정 정보 + +
+
+ 전체: {orderSummary.total} + 일치: {orderSummary.same} + 불일치: {orderSummary.changed}
- - - - {/* 수정 사유 */} - - - - 수정 사유 (필수 ) - - -