From c150d80725ae61ec74641c4ff73405f24a2542f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B6=8C=ED=98=81=EC=84=B1?= Date: Sat, 7 Mar 2026 03:02:30 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20[=ED=92=88=EC=A7=88=EA=B4=80=EB=A6=AC]?= =?UTF-8?q?=20Mock=E2=86=92API=20=EC=A0=84=ED=99=98=20+=20=EA=B2=80?= =?UTF-8?q?=EC=82=AC=20=EB=AA=A8=EB=8B=AC/=EB=AC=B8=EC=84=9C=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - InspectionManagement 전체 API 연동 (Mock 제거) - 제품검사 성적서 8컬럼 동적 렌더링 + FQC 모드 - 제품검사 요청서 양식 기반 렌더링 + Lazy Snapshot - 수주선택 모달 발주처 필터링/비활성화 제약 - 실적신고 snake_case→camelCase 변환 - 공정 단계 검사범위(InspectionScope) 설정 추가 - 빌드 타입 에러 수정 (specification, ProductInspectionData 등) --- .../(protected)/quality/qms/mockData.ts | 1 + .../ImportInspectionInputModal.tsx | 36 +- .../ReceivingManagement/InspectionCreate.tsx | 142 +++--- .../material/ReceivingManagement/actions.ts | 2 + .../process-management/StepForm.tsx | 55 ++ src/components/process-management/actions.ts | 35 ++ .../InspectionManagement/InspectionCreate.tsx | 70 ++- .../InspectionManagement/InspectionDetail.tsx | 313 ++++++++---- .../InspectionManagement/OrderSelectModal.tsx | 40 +- .../ProductInspectionInputModal.tsx | 428 +++++++++++++--- .../quality/InspectionManagement/actions.ts | 331 +++++++----- .../documents/FqcDocumentContent.tsx | 481 +++++++++++++----- .../documents/FqcRequestDocumentContent.tsx | 461 +++++++++++++++++ .../documents/InspectionReportModal.tsx | 53 +- .../documents/InspectionRequestModal.tsx | 123 ++++- .../InspectionManagement/documents/index.ts | 1 + .../InspectionManagement/fqcActions.ts | 47 ++ .../quality/InspectionManagement/mockData.ts | 33 +- .../quality/InspectionManagement/types.ts | 26 + .../PerformanceReportManagement/actions.ts | 85 +++- src/types/process.ts | 31 ++ 21 files changed, 2219 insertions(+), 575 deletions(-) create mode 100644 src/components/quality/InspectionManagement/documents/FqcRequestDocumentContent.tsx diff --git a/src/app/[locale]/(protected)/quality/qms/mockData.ts b/src/app/[locale]/(protected)/quality/qms/mockData.ts index 29fe4632..9ceae951 100644 --- a/src/app/[locale]/(protected)/quality/qms/mockData.ts +++ b/src/app/[locale]/(protected)/quality/qms/mockData.ts @@ -6,6 +6,7 @@ import type { ShipmentDetail } from '@/components/outbound/ShipmentManagement/ty export const MOCK_WORK_ORDER: WorkOrder = { id: 'wo-1', orderNo: 'KD-WO-240924-01', + productCode: 'WY-SC780', productName: '스크린 셔터 (표준형)', processCode: 'screen', processName: 'screen', diff --git a/src/components/material/ReceivingManagement/ImportInspectionInputModal.tsx b/src/components/material/ReceivingManagement/ImportInspectionInputModal.tsx index 593f15c6..ad939d76 100644 --- a/src/components/material/ReceivingManagement/ImportInspectionInputModal.tsx +++ b/src/components/material/ReceivingManagement/ImportInspectionInputModal.tsx @@ -35,6 +35,9 @@ import { type InspectionTemplateResponse, type DocumentResolveResponse, } from './actions'; +import { captureRenderedHtml } from '@/lib/utils/capture-rendered-html'; +import { ImportInspectionDocument } from '@/app/[locale]/(protected)/quality/qms/components/documents/ImportInspectionDocument'; +import type { ImportInspectionTemplate, InspectionItemValue } from '@/app/[locale]/(protected)/quality/qms/components/documents/ImportInspectionDocument'; // ===== Props ===== interface ImportInspectionInputModalProps { @@ -636,7 +639,37 @@ export function ImportInspectionInputModal({ })), ]; - // 4. 저장 API 호출 + // 4. 성적서 문서를 오프스크린 렌더링하여 HTML 스냅샷 캡처 (MNG 출력용) + let renderedHtml: string | undefined; + try { + // 현재 입력값을 ImportInspectionDocument의 initialValues 형식으로 변환 + const docValues: InspectionItemValue[] = template.inspectionItems + .filter(i => i.isFirstInItem !== false) + .map(item => ({ + itemId: item.id, + measurements: Array.from({ length: item.measurementCount }, (_, n) => { + if (item.measurementType === 'okng') { + const v = okngValues[item.id]?.[n]; + return v === 'ok' ? ('OK' as const) : v === 'ng' ? ('NG' as const) : null; + } + const v = measurements[item.id]?.[n]; + return v ? Number(v) : null; + }), + result: getItemResult(item) === 'ok' ? ('OK' as const) : getItemResult(item) === 'ng' ? ('NG' as const) : null, + })); + // 성적서 문서 컴포넌트를 오프스크린에서 렌더링 + renderedHtml = captureRenderedHtml( + + ); + } catch { + // 캡처 실패 시 무시 — rendered_html 없이 저장 진행 + } + + // 5. 저장 API 호출 const result = await saveInspectionData({ templateId: parseInt(template.templateId), itemId, @@ -645,6 +678,7 @@ export function ImportInspectionInputModal({ attachments, receivingId, inspectionResult: overallResult, + rendered_html: renderedHtml, }); if (result.success) { diff --git a/src/components/material/ReceivingManagement/InspectionCreate.tsx b/src/components/material/ReceivingManagement/InspectionCreate.tsx index 905f10d5..95c40421 100644 --- a/src/components/material/ReceivingManagement/InspectionCreate.tsx +++ b/src/components/material/ReceivingManagement/InspectionCreate.tsx @@ -12,12 +12,12 @@ import { useState, useCallback, useMemo, useEffect } from 'react'; import { useRouter } from 'next/navigation'; +import { toast } from 'sonner'; import { getTodayString } from '@/lib/utils/date'; import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate'; import { materialInspectionCreateConfig } from './inspectionConfig'; import { ContentSkeleton } from '@/components/ui/skeleton'; -import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { DatePicker } from '@/components/ui/date-picker'; import { Label } from '@/components/ui/label'; @@ -29,7 +29,6 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select'; -import { Alert, AlertDescription } from '@/components/ui/alert'; import { getReceivings } from './actions'; import type { InspectionCheckItem, ReceivingItem } from './types'; import { SuccessDialog } from './SuccessDialog'; @@ -81,7 +80,7 @@ export function InspectionCreate({ id }: Props) { const [opinion, setOpinion] = useState(''); // 유효성 검사 에러 - const [validationErrors, setValidationErrors] = useState([]); + const [validationErrors, setValidationErrors] = useState>({}); // 성공 다이얼로그 const [showSuccess, setShowSuccess] = useState(false); @@ -117,15 +116,22 @@ export function InspectionCreate({ id }: Props) { // 대상 선택 핸들러 const handleTargetSelect = useCallback((targetId: string) => { setSelectedTargetId(targetId); - setValidationErrors([]); }, []); // 판정 변경 핸들러 - const handleJudgmentChange = useCallback((itemId: string, judgment: '적' | '부적') => { + const handleJudgmentChange = useCallback((itemId: string, index: number, judgment: '적' | '부적') => { setInspectionItems((prev) => prev.map((item) => (item.id === itemId ? { ...item, judgment } : item)) ); - setValidationErrors([]); + // 해당 항목의 에러 클리어 + setValidationErrors((prev) => { + const key = `judgment_${index}`; + if (prev[key]) { + const { [key]: _, ...rest } = prev; + return rest; + } + return prev; + }); }, []); // 비고 변경 핸들러 @@ -137,22 +143,29 @@ export function InspectionCreate({ id }: Props) { // 유효성 검사 const validateForm = useCallback((): boolean => { - const errors: string[] = []; + const errors: Record = {}; // 필수 필드: 검사자 if (!inspector.trim()) { - errors.push('검사자는 필수 입력 항목입니다.'); + errors.inspector = '검사자는 필수 입력 항목입니다.'; } // 검사 항목 판정 확인 inspectionItems.forEach((item, index) => { if (!item.judgment) { - errors.push(`${index + 1}. ${item.name}: 판정을 선택해주세요.`); + errors[`judgment_${index}`] = `${item.name}: 판정을 선택해주세요.`; } }); setValidationErrors(errors); - return errors.length === 0; + + if (Object.keys(errors).length > 0) { + const firstError = Object.values(errors)[0]; + toast.error(firstError); + return false; + } + + return true; }, [inspector, inspectionItems]); // 검사 저장 @@ -214,30 +227,6 @@ export function InspectionCreate({ id }: Props) { {/* 우측: 검사 정보 및 항목 */}
- {/* Validation 에러 표시 */} - {validationErrors.length > 0 && ( - - -
- ⚠️ -
- - 입력 내용을 확인해주세요 ({validationErrors.length}개 오류) - -
    - {validationErrors.map((error, index) => ( -
  • - - {error} -
  • - ))} -
-
-
-
-
- )} - {/* 검사 정보 */}

검사 정보

@@ -257,10 +246,19 @@ export function InspectionCreate({ id }: Props) { value={inspector} onChange={(e) => { setInspector(e.target.value); - setValidationErrors([]); + if (validationErrors.inspector) { + setValidationErrors((prev) => { + const { inspector: _, ...rest } = prev; + return rest; + }); + } }} placeholder="검사자명 입력" + className={validationErrors.inspector ? 'border-red-500' : ''} /> + {validationErrors.inspector && ( +

{validationErrors.inspector}

+ )}
@@ -284,39 +282,45 @@ export function InspectionCreate({ id }: Props) { - {inspectionItems.map((item) => ( - - {item.name} - - {item.specification} - - {item.method} - - - - - handleRemarkChange(item.id, e.target.value)} - placeholder="비고" - className="h-8" - /> - - - ))} + {inspectionItems.map((item, index) => { + const judgmentErrorKey = `judgment_${index}`; + return ( + + {item.name} + + {item.specification} + + {item.method} + + + {validationErrors[judgmentErrorKey] && ( +

{validationErrors[judgmentErrorKey]}

+ )} + + + handleRemarkChange(item.id, e.target.value)} + placeholder="비고" + className="h-8" + /> + + + ); + })}
@@ -361,4 +365,4 @@ export function InspectionCreate({ id }: Props) { renderForm={renderFormContent} /> ); -} \ No newline at end of file +} diff --git a/src/components/material/ReceivingManagement/actions.ts b/src/components/material/ReceivingManagement/actions.ts index adf5d47e..18291598 100644 --- a/src/components/material/ReceivingManagement/actions.ts +++ b/src/components/material/ReceivingManagement/actions.ts @@ -1874,6 +1874,7 @@ export async function saveInspectionData(params: { attachments?: Array<{ file_id: number; attachment_type: string; description?: string }>; receivingId: string; inspectionResult?: 'pass' | 'fail' | null; + rendered_html?: string; }): Promise<{ success: boolean; error?: string; @@ -1889,6 +1890,7 @@ export async function saveInspectionData(params: { title: params.title || '수입검사 성적서', data: params.data, attachments: params.attachments || [], + rendered_html: params.rendered_html, }, errorMessage: '검사 데이터 저장에 실패했습니다.', }); diff --git a/src/components/process-management/StepForm.tsx b/src/components/process-management/StepForm.tsx index 27d45d07..9d88d383 100644 --- a/src/components/process-management/StepForm.tsx +++ b/src/components/process-management/StepForm.tsx @@ -30,12 +30,16 @@ import type { StepConnectionType, StepCompletionType, InspectionSetting, + InspectionScope, + InspectionScopeType, } from '@/types/process'; import { STEP_CONNECTION_TYPE_OPTIONS, STEP_COMPLETION_TYPE_OPTIONS, STEP_CONNECTION_TARGET_OPTIONS, DEFAULT_INSPECTION_SETTING, + DEFAULT_INSPECTION_SCOPE, + INSPECTION_SCOPE_TYPE_OPTIONS, } from '@/types/process'; import { createProcessStep, updateProcessStep } from './actions'; import type { DetailConfig } from '@/components/templates/IntegratedDetailTemplate/types'; @@ -108,6 +112,9 @@ export function StepForm({ mode, processId, initialData }: StepFormProps) { const [inspectionSetting, setInspectionSetting] = useState( initialData?.inspectionSetting || DEFAULT_INSPECTION_SETTING ); + const [inspectionScope, setInspectionScope] = useState( + initialData?.inspectionScope || DEFAULT_INSPECTION_SCOPE + ); // 모달 상태 const [isInspectionSettingOpen, setIsInspectionSettingOpen] = useState(false); @@ -137,6 +144,7 @@ export function StepForm({ mode, processId, initialData }: StepFormProps) { connectionTarget: connectionType === '팝업' ? connectionTarget : undefined, completionType, inspectionSetting: isInspectionEnabled ? inspectionSetting : undefined, + inspectionScope: isInspectionEnabled ? inspectionScope : undefined, }; setIsLoading(true); @@ -237,6 +245,52 @@ export function StepForm({ mode, processId, initialData }: StepFormProps) {
+ {isInspectionEnabled && ( + <> +
+ + +
+ {inspectionScope.type === 'sampling' && ( +
+ + + setInspectionScope((prev) => ({ + ...prev, + sampleSize: Math.max(1, parseInt(e.target.value) || 1), + })) + } + placeholder="검사할 개소 수" + /> +
+ )} + + )}
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} + /> + + {/* 특이사항 */} + +