diff --git a/src/components/quality/InspectionManagement/InspectionCreate.tsx b/src/components/quality/InspectionManagement/InspectionCreate.tsx index 8137146e..83e34b88 100644 --- a/src/components/quality/InspectionManagement/InspectionCreate.tsx +++ b/src/components/quality/InspectionManagement/InspectionCreate.tsx @@ -84,19 +84,45 @@ export function InspectionCreate() { // ===== 수주 선택 처리 ===== const handleOrderSelect = useCallback((items: OrderSelectItem[]) => { - const newOrderItems: OrderSettingItem[] = items.map((item) => ({ - id: item.id, - orderNumber: item.orderNumber, - siteName: item.siteName, - deliveryDate: item.deliveryDate, - floor: '', - symbol: '', - orderWidth: 0, - orderHeight: 0, - constructionWidth: 0, - constructionHeight: 0, - changeReason: '', - })); + const newOrderItems: OrderSettingItem[] = items.flatMap((item) => + item.locations.length > 0 + ? item.locations.map((loc) => ({ + id: `${item.id}-${loc.nodeId}`, + orderId: Number(item.id), + orderNumber: item.orderNumber, + siteName: item.siteName, + clientId: item.clientId, + clientName: item.clientName, + itemId: item.itemId, + itemName: item.itemName, + deliveryDate: item.deliveryDate, + floor: loc.floor, + symbol: loc.symbol, + orderWidth: loc.orderWidth, + orderHeight: loc.orderHeight, + constructionWidth: 0, + constructionHeight: 0, + changeReason: '', + })) + : [{ + id: item.id, + orderId: Number(item.id), + orderNumber: item.orderNumber, + siteName: item.siteName, + clientId: item.clientId, + clientName: item.clientName, + itemId: item.itemId, + itemName: item.itemName, + deliveryDate: item.deliveryDate, + floor: '', + symbol: '', + orderWidth: 0, + orderHeight: 0, + constructionWidth: 0, + constructionHeight: 0, + changeReason: '', + }] + ); setFormData((prev) => ({ ...prev, orderItems: [...prev.orderItems, ...newOrderItems], @@ -659,12 +685,23 @@ export function InspectionCreate() { ), [formData, orderSummary, orderGroups, updateField, updateNested, handleRemoveOrderItem, handleOpenInspectionInput, handleUpdateOrderItemField, orderModalOpen]); - // 이미 선택된 수주 ID 목록 + // 이미 선택된 수주 ID 목록 (orderId 기준, 중복 제거) const excludeOrderIds = useMemo( - () => formData.orderItems.map((item) => item.id), + () => [...new Set(formData.orderItems.map((item) => String(item.orderId ?? item.id)))], [formData.orderItems] ); + // 이미 선택된 수주가 있으면 같은 거래처+모델만 필터 + const orderFilter = useMemo(() => { + if (formData.orderItems.length === 0) return { clientId: undefined, itemId: undefined, label: undefined }; + const first = formData.orderItems[0]; + return { + clientId: first.clientId ?? undefined, + itemId: first.itemId ?? undefined, + label: [first.clientName, first.itemName].filter(Boolean).join(' / ') || undefined, + }; + }, [formData.orderItems]); + return ( <> {/* 제품검사 입력 모달 */} diff --git a/src/components/quality/InspectionManagement/InspectionDetail.tsx b/src/components/quality/InspectionManagement/InspectionDetail.tsx index 7aecaeb3..67eca48a 100644 --- a/src/components/quality/InspectionManagement/InspectionDetail.tsx +++ b/src/components/quality/InspectionManagement/InspectionDetail.tsx @@ -22,7 +22,6 @@ import { Trash2, ChevronDown, ClipboardCheck, - Eye, } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; @@ -50,10 +49,6 @@ import { AlertDialogTitle, } from '@/components/ui/alert-dialog'; import { - Accordion, - AccordionContent, - AccordionItem, - AccordionTrigger, } from '@/components/ui/accordion'; import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate'; import { inspectionConfig } from './inspectionConfig'; @@ -63,6 +58,7 @@ import { getInspectionById, updateInspection, completeInspection, + saveLocationInspection, } from './actions'; import { getFqcStatus, type FqcStatusItem } from './fqcActions'; import { @@ -153,31 +149,54 @@ export function InspectionDetail({ id }: InspectionDetailProps) { // FQC 상태 데이터 (개소별 진행현황) const [fqcStatusItems, setFqcStatusItems] = useState([]); - // 파생: 문서 매핑 (orderItemId → documentId) - const fqcDocumentMap = useMemo(() => { - const map: Record = {}; - fqcStatusItems.forEach((item) => { - if (item.documentId) map[String(item.orderItemId)] = item.documentId; - }); - return map; - }, [fqcStatusItems]); - // 파생: 진행현황 통계 - const fqcStats = useMemo(() => { - if (fqcStatusItems.length === 0) return null; - return { - total: fqcStatusItems.length, - passed: fqcStatusItems.filter((i) => i.judgement === '합격').length, - failed: fqcStatusItems.filter((i) => i.judgement === '불합격').length, - inProgress: fqcStatusItems.filter((i) => i.documentId != null && !i.judgement).length, - notCreated: fqcStatusItems.filter((i) => i.documentId == null).length, - }; - }, [fqcStatusItems]); + // 개소별 검사 상태 집계 (legacy inspectionData + FQC 통합) + const inspectionStats = useMemo(() => { + const items = isEditMode ? formData.orderItems : (inspection?.orderItems || []); + if (items.length === 0) return null; - // 개소별 FQC 상태 조회 헬퍼 + const getStatus = (item: OrderSettingItem) => { + // FQC 문서 기반 상태 확인 + const fqcItem = fqcStatusItems.find( + (i) => i.floorCode === item.floor && i.symbolCode === item.symbol + ); + if (fqcItem?.judgement === '합격') return 'passed'; + if (fqcItem?.judgement === '불합격') return 'failed'; + if (fqcItem?.documentId) return 'inProgress'; + + // legacy inspectionData 확인 + if (!item.inspectionData) return 'none'; + const d = item.inspectionData; + const judgmentFields = [ + d.appearanceProcessing, d.appearanceSewing, d.appearanceAssembly, + d.appearanceSmokeBarrier, d.appearanceBottomFinish, d.motor, d.material, + d.lengthJudgment, d.heightJudgment, d.guideRailGap, d.bottomFinishGap, + d.fireResistanceTest, d.smokeLeakageTest, d.openCloseTest, d.impactTest, + ]; + const inspected = judgmentFields.filter(v => v !== null && v !== undefined); + const hasPhotos = d.productImages && d.productImages.length > 0; + if (inspected.length === 0 && !hasPhotos) return 'none'; + if (inspected.length < judgmentFields.length || !hasPhotos) return 'inProgress'; + if (inspected.some(v => v === 'fail')) return 'failed'; + return 'passed'; + }; + + const statuses = items.map(getStatus); + return { + total: items.length, + passed: statuses.filter(s => s === 'passed').length, + failed: statuses.filter(s => s === 'failed').length, + inProgress: statuses.filter(s => s === 'inProgress').length, + none: statuses.filter(s => s === 'none').length, + }; + }, [isEditMode, formData.orderItems, inspection?.orderItems, fqcStatusItems]); + + // 개소별 FQC 상태 조회 헬퍼 (floor+symbol 기반 매칭) const getFqcItemStatus = useCallback( - (orderItemId: string): FqcStatusItem | null => { - return fqcStatusItems.find((i) => String(i.orderItemId) === orderItemId) ?? null; + (item: OrderSettingItem): FqcStatusItem | null => { + return fqcStatusItems.find( + (i) => i.floorCode === item.floor && i.symbolCode === item.symbol + ) ?? null; }, [fqcStatusItems] ); @@ -316,19 +335,45 @@ export function InspectionDetail({ id }: InspectionDetailProps) { // ===== 수주 선택/삭제 처리 ===== const handleOrderSelect = useCallback((items: OrderSelectItem[]) => { - const newOrderItems: OrderSettingItem[] = items.map((item) => ({ - id: item.id, - orderNumber: item.orderNumber, - siteName: item.siteName, - deliveryDate: item.deliveryDate, - floor: '', - symbol: '', - orderWidth: 0, - orderHeight: 0, - constructionWidth: 0, - constructionHeight: 0, - changeReason: '', - })); + const newOrderItems: OrderSettingItem[] = items.flatMap((item) => + item.locations.length > 0 + ? item.locations.map((loc) => ({ + id: `${item.id}-${loc.nodeId}`, + orderId: Number(item.id), + orderNumber: item.orderNumber, + siteName: item.siteName, + clientId: item.clientId, + clientName: item.clientName, + itemId: item.itemId, + itemName: item.itemName, + deliveryDate: item.deliveryDate, + floor: loc.floor, + symbol: loc.symbol, + orderWidth: loc.orderWidth, + orderHeight: loc.orderHeight, + constructionWidth: 0, + constructionHeight: 0, + changeReason: '', + })) + : [{ + id: item.id, + orderId: Number(item.id), + orderNumber: item.orderNumber, + siteName: item.siteName, + clientId: item.clientId, + clientName: item.clientName, + itemId: item.itemId, + itemName: item.itemName, + deliveryDate: item.deliveryDate, + floor: '', + symbol: '', + orderWidth: 0, + orderHeight: 0, + constructionWidth: 0, + constructionHeight: 0, + changeReason: '', + }] + ); setFormData((prev) => ({ ...prev, orderItems: [...prev.orderItems, ...newOrderItems], @@ -342,11 +387,24 @@ export function InspectionDetail({ id }: InspectionDetailProps) { })); }, []); + // 이미 선택된 수주 ID 목록 (orderId 기준, 중복 제거) const excludeOrderIds = useMemo( - () => formData.orderItems.map((item) => item.id), + () => [...new Set(formData.orderItems.map((item) => String(item.orderId ?? item.id)))], [formData.orderItems] ); + // 이미 선택된 수주가 있으면 같은 거래처+모델만 필터 + const orderFilter = useMemo(() => { + const items = isEditMode ? formData.orderItems : (inspection?.orderItems || []); + if (items.length === 0) return { clientId: undefined, itemId: undefined, label: undefined }; + const first = items[0]; + return { + clientId: first.clientId ?? undefined, + itemId: first.itemId ?? undefined, + label: [first.clientName, first.itemName].filter(Boolean).join(' / ') || undefined, + }; + }, [isEditMode, formData.orderItems, inspection?.orderItems]); + // ===== 수주 설정 요약 ===== const orderSummary = useMemo(() => { const items = isEditMode ? formData.orderItems : (inspection?.orderItems || []); @@ -384,22 +442,46 @@ export function InspectionDetail({ id }: InspectionDetailProps) { setInspectionInputOpen(true); }, []); - const handleInspectionComplete = useCallback((data: ProductInspectionData) => { + const handleInspectionComplete = useCallback(async ( + data: ProductInspectionData, + constructionInfo?: { width: number | null; height: number | null; changeReason: string } + ) => { if (!selectedOrderItem) return; - // formData의 해당 orderItem에 inspectionData 저장 + // 서버에 개소별 검사 데이터 저장 + const result = await saveLocationInspection(id, selectedOrderItem.id, data, constructionInfo); + if (!result.success) { + toast.error(result.error || '검사 데이터 저장에 실패했습니다.'); + return; + } + + const updateItem = (item: OrderSettingItem) => { + if (item.id !== selectedOrderItem.id) return item; + const updated = { ...item, inspectionData: data }; + if (constructionInfo) { + if (constructionInfo.width !== null) updated.constructionWidth = constructionInfo.width; + if (constructionInfo.height !== null) updated.constructionHeight = constructionInfo.height; + updated.changeReason = constructionInfo.changeReason; + } + return updated; + }; + + // 로컬 state도 반영 setFormData((prev) => ({ ...prev, - orderItems: prev.orderItems.map((item) => - item.id === selectedOrderItem.id - ? { ...item, inspectionData: data } - : item - ), + orderItems: prev.orderItems.map(updateItem), })); + // inspection 데이터도 갱신 (새로고침 없이 반영) + if (inspection) { + setInspection({ + ...inspection, + orderItems: inspection.orderItems.map(updateItem), + }); + } + toast.success('검사 데이터가 저장되었습니다.'); - setSelectedOrderItem(null); - }, [selectedOrderItem]); + }, [id, selectedOrderItem, inspection]); // ===== 시공규격/변경사유 수정 핸들러 (수정 모드) ===== const handleUpdateOrderItemField = useCallback(( @@ -418,25 +500,47 @@ export function InspectionDetail({ id }: InspectionDetailProps) { // ===== FQC 상태 뱃지 렌더링 ===== const renderFqcBadge = useCallback( (item: OrderSettingItem) => { - const fqcItem = getFqcItemStatus(item.id); + const fqcItem = getFqcItemStatus(item); if (!fqcItem) { // FQC 데이터 없음 → legacy 상태 return item.inspectionData ? ( 검사완료 ) : ( - 미검사 + 미검사 ); } if (fqcItem.judgement === '합격') { - return 합격; + return 합격; } if (fqcItem.judgement === '불합격') { - return 불합격; + return 불합격; } if (fqcItem.documentId) { - return 진행중; + return 진행중; } - return 미생성; + // FQC 문서 없음 → legacy 검사 데이터 확인 + if (!item.inspectionData) { + return 미검사; + } + const d = item.inspectionData; + const judgmentFields = [ + d.appearanceProcessing, d.appearanceSewing, d.appearanceAssembly, + d.appearanceSmokeBarrier, d.appearanceBottomFinish, d.motor, d.material, + d.lengthJudgment, d.heightJudgment, d.guideRailGap, d.bottomFinishGap, + d.fireResistanceTest, d.smokeLeakageTest, d.openCloseTest, d.impactTest, + ]; + const inspected = judgmentFields.filter(v => v !== null && v !== undefined); + const hasPhotos = d.productImages && d.productImages.length > 0; + if (inspected.length === 0 && !hasPhotos) { + return 미검사; + } + if (inspected.length < judgmentFields.length || !hasPhotos) { + return 진행중; + } + if (inspected.some(v => v === 'fail')) { + return 불합격; + } + return 합격; }, [getFqcItemStatus] ); @@ -451,15 +555,15 @@ export function InspectionDetail({ id }: InspectionDetailProps) { // ===== FQC 진행현황 통계 바 ===== const renderFqcProgressBar = useMemo(() => { - if (!fqcStats) return null; - const { total, passed, failed, inProgress, notCreated } = fqcStats; + if (!inspectionStats) return null; + const { total, passed, failed, inProgress, none } = inspectionStats; return (
합격 {passed} 불합격 {failed} 진행중 {inProgress} - 미생성 {notCreated} + 미검사 {none}
{passed > 0 && ( @@ -483,7 +587,7 @@ export function InspectionDetail({ id }: InspectionDetailProps) {
); - }, [fqcStats]); + }, [inspectionStats]); // ===== 수주 설정 아코디언 (조회 모드) ===== const renderOrderAccordion = (groups: OrderGroup[]) => { @@ -496,20 +600,16 @@ export function InspectionDetail({ id }: InspectionDetailProps) { } return ( - +
{groups.map((group, groupIndex) => ( - - {/* 상위 레벨: 수주번호, 현장명, 납품일, 개소 */} - -
- {group.orderNumber} - {group.siteName} - {group.deliveryDate} - {group.locationCount}개소 -
-
- - {/* 하위 레벨: 테이블 */} +
+
+ {group.orderNumber} + {group.siteName} + {group.deliveryDate} + {group.locationCount}개소 +
+
@@ -538,15 +638,14 @@ export function InspectionDetail({ id }: InspectionDetailProps) {
{renderFqcBadge(item)} - {(getFqcItemStatus(item.id) || item.inspectionData) && ( - + 보기 + )}
@@ -554,10 +653,10 @@ export function InspectionDetail({ id }: InspectionDetailProps) { ))}
- - +
+
))} - +
); }; @@ -572,33 +671,27 @@ export function InspectionDetail({ id }: InspectionDetailProps) { } return ( - +
{groups.map((group, groupIndex) => ( - - {/* 상위 레벨: 수주번호, 현장명, 납품일, 개소, 삭제 */} -
- -
- {group.orderNumber} - {group.siteName} - {group.deliveryDate} - {group.locationCount}개소 -
-
+
+
+ {group.orderNumber} + {group.siteName} + {group.deliveryDate} + {group.locationCount}개소
- +
{/* 하위 레벨: 테이블 (시공규격, 변경사유 편집 가능) */} @@ -670,10 +763,10 @@ export function InspectionDetail({ id }: InspectionDetailProps) { ))}
- - +
+
))} - +
); }; @@ -845,8 +938,6 @@ export function InspectionDetail({ id }: InspectionDetailProps) { 수주 설정 정보
전체: {orderSummary.total} - 일치: {orderSummary.same} - 불일치: {orderSummary.changed}
{renderFqcProgressBar} @@ -1151,8 +1242,6 @@ export function InspectionDetail({ id }: InspectionDetailProps) {
전체: {orderSummary.total} - 일치: {orderSummary.same} - 불일치: {orderSummary.changed}
{renderFqcProgressBar} @@ -1240,6 +1329,9 @@ export function InspectionDetail({ id }: InspectionDetailProps) { onOpenChange={setOrderModalOpen} onSelect={handleOrderSelect} excludeIds={excludeOrderIds} + filterClientId={orderFilter.clientId} + filterItemId={orderFilter.itemId} + filterLabel={orderFilter.label} /> {/* 제품검사요청서 모달 */} @@ -1257,19 +1349,23 @@ export function InspectionDetail({ id }: InspectionDetailProps) { data={inspection ? buildReportDocumentData(inspection, isEditMode ? formData.orderItems : undefined) : null} inspection={inspection} orderItems={isEditMode ? formData.orderItems : inspection?.orderItems} - fqcDocumentMap={Object.keys(fqcDocumentMap).length > 0 ? fqcDocumentMap : undefined} /> {/* 제품검사 입력 모달 */} { setInspectionInputOpen(open); if (!open) setSelectedOrderItem(null); }} orderItemId={selectedOrderItem?.id || ''} productName="방화셔터" specification={selectedOrderItem ? `${selectedOrderItem.orderWidth}x${selectedOrderItem.orderHeight}` : ''} initialData={selectedOrderItem?.inspectionData} onComplete={handleInspectionComplete} - fqcDocumentId={selectedOrderItem ? fqcDocumentMap[selectedOrderItem.id] ?? null : null} + fqcDocumentId={selectedOrderItem?.documentId ?? null} + constructionWidth={selectedOrderItem?.constructionWidth} + constructionHeight={selectedOrderItem?.constructionHeight} + changeReason={selectedOrderItem?.changeReason} + orderItems={isEditMode ? formData.orderItems : (inspection?.orderItems || [])} + onNavigate={(item) => setSelectedOrderItem(item)} /> ); diff --git a/src/components/quality/InspectionManagement/OrderSelectModal.tsx b/src/components/quality/InspectionManagement/OrderSelectModal.tsx index 8047386c..ec5960e9 100644 --- a/src/components/quality/InspectionManagement/OrderSelectModal.tsx +++ b/src/components/quality/InspectionManagement/OrderSelectModal.tsx @@ -30,6 +30,12 @@ interface OrderSelectModalProps { onSelect: (items: OrderSelectItem[]) => void; /** 이미 선택된 항목 ID 목록 (중복 선택 방지) */ excludeIds?: string[]; + /** 같은 거래처만 필터 (이미 선택된 수주의 client_id) */ + filterClientId?: number | null; + /** 같은 모델만 필터 (이미 선택된 수주의 item_id) */ + filterItemId?: number | null; + /** 필터 안내 텍스트 (예: "발주처A / 방화셔터") */ + filterLabel?: string; } export function OrderSelectModal({ @@ -37,10 +43,17 @@ export function OrderSelectModal({ onOpenChange, onSelect, excludeIds = [], + filterClientId, + filterItemId, + filterLabel, }: OrderSelectModalProps) { const handleFetchData = useCallback(async (query: string) => { try { - const result = await getOrderSelectList({ q: query || undefined }); + const result = await getOrderSelectList({ + q: query || undefined, + clientId: filterClientId, + itemId: filterItemId, + }); if (result.success) { return result.data.filter((item) => !excludeIds.includes(item.id)); } @@ -52,13 +65,13 @@ export function OrderSelectModal({ toast.error('수주 목록 로드 중 오류가 발생했습니다.'); return []; } - }, [excludeIds]); + }, [excludeIds, filterClientId, filterItemId]); return ( open={open} onOpenChange={onOpenChange} - title="수주 선택" + title={filterLabel ? `수주 선택 — ${filterLabel}` : '수주 선택'} searchPlaceholder="수주번호, 현장명 검색..." fetchData={handleFetchData} keyExtractor={(item) => item.id} @@ -71,9 +84,12 @@ export function OrderSelectModal({ confirmLabel="선택" allowSelectAll isItemDisabled={(item, selectedItems) => { + // 서버 필터가 이미 적용된 경우 모달 내 추가 제한 불필요 + if (filterClientId || filterItemId) return false; + // 서버 필터 없이 첫 선택 시 모달 내에서 같은 거래처+모델만 선택 가능 if (selectedItems.length === 0) return false; - const selectedClient = selectedItems[0].clientName; - return item.clientName !== selectedClient; + const first = selectedItems[0]; + return item.clientId !== first.clientId || item.itemId !== first.itemId; }} listWrapper={(children, selectState) => ( @@ -90,24 +106,25 @@ export function OrderSelectModal({ 수주번호현장명발주처 + 모델납품일개소 {children} - {/* 빈 상태는 공통 컴포넌트에서 처리 */}
)} renderItem={(item, isSelected, isDisabled) => ( - + e.stopPropagation()}> {item.orderNumber} {item.siteName} {item.clientName} + {item.itemName} {item.deliveryDate} {item.locationCount} diff --git a/src/components/quality/InspectionManagement/ProductInspectionInputModal.tsx b/src/components/quality/InspectionManagement/ProductInspectionInputModal.tsx index 65eae9d2..185d5377 100644 --- a/src/components/quality/InspectionManagement/ProductInspectionInputModal.tsx +++ b/src/components/quality/InspectionManagement/ProductInspectionInputModal.tsx @@ -19,15 +19,21 @@ import { DialogTitle, } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; -import { Loader2 } from 'lucide-react'; +import { Loader2, ChevronLeft, ChevronRight } from 'lucide-react'; import { cn } from '@/lib/utils'; import { toast } from 'sonner'; import { getFqcTemplate, getFqcDocument, saveFqcDocument } from './fqcActions'; import type { FqcTemplate, FqcTemplateItem, FqcDocumentData } from './fqcActions'; -import type { ProductInspectionData } from './types'; +import type { ProductInspectionData, OrderSettingItem } from './types'; type JudgmentValue = '적합' | '부적합' | null; +interface ConstructionInfo { + width: number | null; + height: number | null; + changeReason: string; +} + interface ProductInspectionInputModalProps { open: boolean; onOpenChange: (open: boolean) => void; @@ -35,9 +41,18 @@ interface ProductInspectionInputModalProps { productName?: string; specification?: string; initialData?: ProductInspectionData; - onComplete: (data: ProductInspectionData) => void; + onComplete: (data: ProductInspectionData, constructionInfo?: ConstructionInfo) => void; /** FQC 문서 ID (있으면 양식 기반 모드) */ fqcDocumentId?: number | null; + /** 시공 가로/세로 초기값 */ + constructionWidth?: number | null; + constructionHeight?: number | null; + /** 변경사유 초기값 */ + changeReason?: string; + /** 전체 주문 아이템 목록 (이전/다음 네비게이션용) */ + orderItems?: OrderSettingItem[]; + /** 이전/다음 이동 시 호출 (저장 후 해당 아이템으로 전환) */ + onNavigate?: (item: OrderSettingItem) => void; } export function ProductInspectionInputModal({ @@ -49,6 +64,11 @@ export function ProductInspectionInputModal({ initialData, onComplete, fqcDocumentId, + constructionWidth: initialConstructionWidth, + constructionHeight: initialConstructionHeight, + changeReason: initialChangeReason = '', + orderItems = [], + onNavigate, }: ProductInspectionInputModalProps) { // FQC 모드 상태 const [fqcTemplate, setFqcTemplate] = useState(null); @@ -56,6 +76,11 @@ export function ProductInspectionInputModal({ const [isLoadingFqc, setIsLoadingFqc] = useState(false); const [isSaving, setIsSaving] = useState(false); + // 시공 가로/세로/변경사유 + const [conWidth, setConWidth] = useState(null); + const [conHeight, setConHeight] = useState(null); + const [changeReason, setChangeReason] = useState(''); + // 판정 상태 (FQC 모드) const [judgments, setJudgments] = useState>({}); @@ -94,6 +119,15 @@ export function ProductInspectionInputModal({ .finally(() => setIsLoadingFqc(false)); }, [open, useFqcMode, fqcDocumentId]); + // 모달 열릴 때 또는 아이템 전환 시 시공 사이즈/변경사유 초기화 + useEffect(() => { + if (open) { + setConWidth(initialConstructionWidth ?? null); + setConHeight(initialConstructionHeight ?? null); + setChangeReason(initialChangeReason); + } + }, [open, orderItemId, initialConstructionWidth, initialConstructionHeight, initialChangeReason]); + // 모달 닫힐 때 상태 초기화 useEffect(() => { if (!open) { @@ -125,7 +159,7 @@ export function ProductInspectionInputModal({ }, [fqcTemplate, judgments]); // FQC 검사 완료 (서버 저장) - const handleFqcComplete = useCallback(async () => { + const handleFqcComplete = useCallback(async (closeModal = true) => { if (!fqcTemplate || !fqcDocumentId) return; const dataSection = fqcTemplate.sections.find(s => s.items.length > 0); @@ -134,7 +168,6 @@ export function ProductInspectionInputModal({ setIsSaving(true); try { - // document_data 형식으로 변환 const records: Array<{ section_id: number | null; column_id: number | null; @@ -156,7 +189,6 @@ export function ProductInspectionInputModal({ } }); - // 종합판정 records.push({ section_id: null, column_id: null, @@ -173,14 +205,10 @@ export function ProductInspectionInputModal({ if (result.success) { toast.success('검사 데이터가 저장되었습니다.'); - // onComplete callback으로 로컬 상태도 업데이트 - // Legacy 타입 호환: FQC 판정 데이터를 ProductInspectionData 형태로 변환 const legacyData: ProductInspectionData = { productName, specification, productImages: [], - // FQC 모드에서는 모든 항목을 적합/부적합으로만 판정 - // 11개 항목을 legacy 필드에 매핑 (가능한 만큼) appearanceProcessing: judgments[0] === '적합' ? 'pass' : judgments[0] === '부적합' ? 'fail' : null, appearanceSewing: judgments[1] === '적합' ? 'pass' : judgments[1] === '부적합' ? 'fail' : null, appearanceAssembly: judgments[2] === '적합' ? 'pass' : judgments[2] === '부적합' ? 'fail' : null, @@ -204,22 +232,15 @@ export function ProductInspectionInputModal({ specialNotes: '', }; - onComplete(legacyData); - onOpenChange(false); + onComplete(legacyData, { width: conWidth, height: conHeight, changeReason }); + if (closeModal) onOpenChange(false); } else { toast.error(result.error || '검사 데이터 저장에 실패했습니다.'); } } finally { setIsSaving(false); } - }, [fqcTemplate, fqcDocumentId, judgments, overallJudgment, productName, specification, onComplete, onOpenChange]); - - // Legacy 완료 핸들러 - const handleLegacyComplete = useCallback(() => { - if (!legacyFormData) return; - onComplete(legacyFormData); - onOpenChange(false); - }, [onComplete, onOpenChange]); + }, [fqcTemplate, fqcDocumentId, judgments, overallJudgment, productName, specification, conWidth, conHeight, changeReason, onComplete, onOpenChange]); // ===== Legacy 모드 상태 ===== const [legacyFormData, setLegacyFormData] = useState(null); @@ -230,30 +251,76 @@ export function ProductInspectionInputModal({ productName, specification, productImages: [], - appearanceProcessing: 'pass', - appearanceSewing: 'pass', - appearanceAssembly: 'pass', - appearanceSmokeBarrier: 'pass', - appearanceBottomFinish: 'pass', - motor: 'pass', - material: 'pass', + appearanceProcessing: null, + appearanceSewing: null, + appearanceAssembly: null, + appearanceSmokeBarrier: null, + appearanceBottomFinish: null, + motor: null, + material: null, lengthValue: null, - lengthJudgment: 'pass', + lengthJudgment: null, heightValue: null, - heightJudgment: 'pass', + heightJudgment: null, guideRailGapValue: null, - guideRailGap: 'pass', + guideRailGap: null, bottomFinishGapValue: null, - bottomFinishGap: 'pass', - fireResistanceTest: 'pass', - smokeLeakageTest: 'pass', - openCloseTest: 'pass', - impactTest: 'pass', + bottomFinishGap: null, + fireResistanceTest: null, + smokeLeakageTest: null, + openCloseTest: null, + impactTest: null, hasSpecialNotes: false, specialNotes: '', }); } - }, [open, useFqcMode, initialData, productName, specification]); + }, [open, orderItemId, useFqcMode, initialData, productName, specification]); + + // Legacy 완료 핸들러 + const handleLegacyComplete = useCallback(() => { + if (!legacyFormData) return; + onComplete(legacyFormData, { width: conWidth, height: conHeight, changeReason }); + onOpenChange(false); + }, [legacyFormData, conWidth, conHeight, changeReason, onComplete, onOpenChange]); + + // ===== 이전/다음 네비게이션 ===== + const currentIndex = orderItems.findIndex(item => item.id === orderItemId); + const totalItems = orderItems.length; + const hasPrev = currentIndex > 0; + const hasNext = currentIndex < totalItems - 1; + + const hasLegacyChanges = useCallback(() => { + if (!legacyFormData) return false; + // 검사 데이터 변경 확인 + if (JSON.stringify(legacyFormData) !== JSON.stringify(initialData ?? null)) return true; + // 시공 사이즈/변경사유 변경 확인 + if (conWidth !== (initialConstructionWidth ?? null)) return true; + if (conHeight !== (initialConstructionHeight ?? null)) return true; + if (changeReason !== initialChangeReason) return true; + return false; + }, [legacyFormData, initialData, conWidth, conHeight, changeReason, initialConstructionWidth, initialConstructionHeight, initialChangeReason]); + + const saveAndNavigate = useCallback(async (targetItem: OrderSettingItem) => { + if (!onNavigate) return; + // 변경된 내용이 있을 때만 저장 + if (useFqcMode) { + // FQC: judgments 변경 확인 + const hasJudgmentChanges = Object.keys(judgments).length > 0; + if (hasJudgmentChanges) await handleFqcComplete(false); + } else if (legacyFormData && hasLegacyChanges()) { + onComplete(legacyFormData, { width: conWidth, height: conHeight, changeReason }); + } + // 다음 아이템으로 이동 + onNavigate(targetItem); + }, [useFqcMode, handleFqcComplete, legacyFormData, judgments, conWidth, conHeight, changeReason, hasLegacyChanges, onComplete, onNavigate]); + + const handlePrev = useCallback(() => { + if (hasPrev) saveAndNavigate(orderItems[currentIndex - 1]); + }, [hasPrev, currentIndex, orderItems, saveAndNavigate]); + + const handleNext = useCallback(() => { + if (hasNext) saveAndNavigate(orderItems[currentIndex + 1]); + }, [hasNext, currentIndex, orderItems, saveAndNavigate]); // FQC 데이터 섹션 const dataSection = fqcTemplate?.sections.find(s => s.items.length > 0); @@ -274,6 +341,41 @@ export function ProductInspectionInputModal({ + {/* 이전/다음 네비게이션 */} + {totalItems > 1 && ( +
+ +
+ {currentIndex + 1} + / {totalItems} + {orderItems[currentIndex] && ( + + ({orderItems[currentIndex].floor}-{orderItems[currentIndex].symbol}) + + )} +
+ +
+ )} +
{/* 제품명 / 규격 */}
@@ -287,6 +389,40 @@ export function ProductInspectionInputModal({
+ {/* 시공 가로/세로 + 변경사유 */} +
+
+ 시공 가로 + setConWidth(e.target.value ? Number(e.target.value) : null)} + className="w-full h-9 px-3 rounded-md border border-input bg-background text-sm" + placeholder="가로" + /> +
+
+ 시공 세로 + setConHeight(e.target.value ? Number(e.target.value) : null)} + className="w-full h-9 px-3 rounded-md border border-input bg-background text-sm" + placeholder="세로" + /> +
+
+ 변경사유 + setChangeReason(e.target.value)} + className="w-full h-9 px-3 rounded-md border border-input bg-background text-sm" + placeholder="변경사유 입력" + /> +
+
+ {useFqcMode ? ( // ===== FQC 양식 기반 모드 ===== isLoadingFqc ? ( @@ -298,11 +434,37 @@ export function ProductInspectionInputModal({ <> {/* 검사항목 목록 (template 기반) */}
-
- {dataSection?.title || dataSection?.name || '검사항목'} - - ({sortedItems.length}항목) - +
+
+ {dataSection?.title || dataSection?.name || '검사항목'} + + ({sortedItems.length}항목) + +
+ {(() => { + const allPassed = sortedItems.length > 0 && sortedItems.every((_, idx) => judgments[idx] === '적합'); + return allPassed ? ( + + ) : ( + + ); + })()}
@@ -353,7 +515,7 @@ export function ProductInspectionInputModal({ 취소 + ) : ( + + )} +
+ {/* 1. 겉모양 검사 */} + + update('appearanceProcessing', v)} /> + update('appearanceSewing', v)} /> + update('appearanceAssembly', v)} /> + update('appearanceSmokeBarrier', v)} /> + update('appearanceBottomFinish', v)} /> - {/* 재질/치수 검사 */} - - update('material', v)} /> - update('lengthJudgment', v)} /> - update('heightJudgment', v)} /> - update('guideRailGap', v)} /> - update('bottomFinishGap', v)} /> + {/* 2. 모터 */} + + update('motor', v)} /> - {/* 시험 검사 */} - - update('fireResistanceTest', v)} /> - update('smokeLeakageTest', v)} /> - update('openCloseTest', v)} /> - update('impactTest', v)} /> + {/* 3. 재질 */} + + update('material', v)} /> + + {/* 4. 치수(오픈사이즈) */} + + update('lengthJudgment', v)} /> + update('heightJudgment', v)} /> + update('guideRailGap', v)} /> + update('bottomFinishGap', v)} /> + + {/* 5~9. 시험 검사 */} + + update('fireResistanceTest', v)} /> + update('smokeLeakageTest', v)} /> + update('openCloseTest', v)} /> + update('impactTest', v)} /> + + {/* 사진 첨부 */} + + update('productImages', images)} + maxCount={2} + /> + + {/* 특이사항 */} + +