= {};
// 필수 필드: 검사자
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}
+ )}
@@ -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="검사할 개소 수"
+ />
+
+ )}
+ >
+ )}
), [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 2e7319a8..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}
/>
{/* 제품검사요청서 모달 */}
@@ -1247,6 +1339,7 @@ export function InspectionDetail({ id }: InspectionDetailProps) {
open={requestDocOpen}
onOpenChange={setRequestDocOpen}
data={inspection ? buildRequestDocumentData(inspection) : null}
+ requestDocumentId={inspection?.requestDocumentId}
/>
{/* 제품검사성적서 모달 */}
@@ -1256,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 4821eceb..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,24 +65,32 @@ 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}
searchMode="enter"
loadOnOpen
- dialogClassName="sm:max-w-2xl"
+ dialogClassName="sm:max-w-3xl"
listContainerClassName="max-h-[400px] overflow-y-auto border rounded-md"
mode="multiple"
onSelect={onSelect}
confirmLabel="선택"
allowSelectAll
+ isItemDisabled={(item, selectedItems) => {
+ // 서버 필터가 이미 적용된 경우 모달 내 추가 제한 불필요
+ if (filterClientId || filterItemId) return false;
+ // 서버 필터 없이 첫 선택 시 모달 내에서 같은 거래처+모델만 선택 가능
+ if (selectedItems.length === 0) return false;
+ const first = selectedItems[0];
+ return item.clientId !== first.clientId || item.itemId !== first.itemId;
+ }}
listWrapper={(children, selectState) => (
@@ -84,23 +105,26 @@ export function OrderSelectModal({
수주번호
현장명
+ 발주처
+ 모델
납품일
개소
{children}
- {/* 빈 상태는 공통 컴포넌트에서 처리 */}
)}
- renderItem={(item, isSelected) => (
-
+ 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({
+ {/* 시공 가로/세로 + 변경사유 */}
+
+
{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({
취소