+
+
{label}
+ {criteria &&
{criteria}
}
+
+
);
}
+
+function LegacyPhotoUpload({
+ images,
+ onChange,
+ maxCount,
+}: {
+ images: string[];
+ onChange: (images: string[]) => void;
+ maxCount: number;
+}) {
+ const fileInputRef = useRef
(null);
+
+ const handleFileChange = (e: React.ChangeEvent) => {
+ const files = e.target.files;
+ if (!files) return;
+
+ Array.from(files).forEach((file) => {
+ if (images.length >= maxCount) return;
+ const reader = new FileReader();
+ reader.onload = (ev) => {
+ const dataUrl = ev.target?.result as string;
+ onChange([...images, dataUrl].slice(0, maxCount));
+ };
+ reader.readAsDataURL(file);
+ });
+ e.target.value = '';
+ };
+
+ const removeImage = (index: number) => {
+ onChange(images.filter((_, i) => i !== index));
+ };
+
+ return (
+
+ {images.map((src, idx) => (
+
+

+
+
+ ))}
+ {images.length < maxCount && (
+
+ )}
+
+
+ );
+}
diff --git a/src/components/quality/InspectionManagement/actions.ts b/src/components/quality/InspectionManagement/actions.ts
index 2d81752e..c803f0cb 100644
--- a/src/components/quality/InspectionManagement/actions.ts
+++ b/src/components/quality/InspectionManagement/actions.ts
@@ -85,8 +85,13 @@ interface ProductInspectionApi {
};
order_items: Array<{
id: string;
+ order_id?: number;
order_number: string;
site_name: string;
+ client_id?: number | null;
+ client_name?: string;
+ item_id?: number | null;
+ item_name?: string;
delivery_date: string;
floor: string;
symbol: string;
@@ -128,9 +133,19 @@ interface OrderSelectItemApi {
id: number;
order_number: string;
site_name: string;
+ client_id: number | null;
client_name: string;
+ item_id: number | null;
+ item_name: string;
delivery_date: string;
location_count: number;
+ locations: Array<{
+ node_id: number;
+ floor: string;
+ symbol: string;
+ order_width: number;
+ order_height: number;
+ }>;
}
// ===== 페이지네이션 =====
@@ -224,6 +239,10 @@ function transformApiToFrontend(api: ProductInspectionApi): ProductInspection {
orderId: item.order_id,
orderNumber: item.order_number,
siteName: item.site_name || '',
+ clientId: item.client_id ?? null,
+ clientName: item.client_name ?? '',
+ itemId: item.item_id ?? null,
+ itemName: item.item_name ?? '',
deliveryDate: item.delivery_date || '',
floor: item.floor,
symbol: item.symbol,
@@ -232,6 +251,7 @@ function transformApiToFrontend(api: ProductInspectionApi): ProductInspection {
constructionWidth: item.construction_width,
constructionHeight: item.construction_height,
changeReason: item.change_reason,
+ documentId: item.document_id ?? null,
inspectionData: item.inspection_data || undefined,
})),
requestDocumentId: api.request_document_id ?? null,
@@ -591,6 +611,42 @@ export async function updateInspection(
: { success: true };
}
+// ===== 개소별 검사 저장 =====
+
+export async function saveLocationInspection(
+ docId: string,
+ locationId: string,
+ inspectionData: Record,
+ constructionInfo?: {
+ width: number | null;
+ height: number | null;
+ changeReason: string;
+ },
+): Promise<{
+ success: boolean;
+ error?: string;
+ __authError?: boolean;
+}> {
+ const body: Record = {
+ inspection_data: inspectionData,
+ inspection_status: 'completed',
+ };
+ if (constructionInfo) {
+ body.construction_width = constructionInfo.width;
+ body.construction_height = constructionInfo.height;
+ body.change_reason = constructionInfo.changeReason;
+ }
+
+ const result = await executeServerAction({
+ url: buildApiUrl(`/api/v1/quality/documents/${docId}/locations/${locationId}/inspect`),
+ method: 'POST',
+ body,
+ errorMessage: '검사 데이터 저장에 실패했습니다.',
+ });
+
+ return { success: result.success, error: result.error, __authError: result.__authError };
+}
+
// ===== 삭제 =====
export async function deleteInspection(id: string): Promise<{
@@ -640,6 +696,8 @@ export async function completeInspection(
export async function getOrderSelectList(params?: {
q?: string;
+ clientId?: number | null;
+ itemId?: number | null;
}): Promise<{
success: boolean;
data: OrderSelectItem[];
@@ -647,7 +705,11 @@ export async function getOrderSelectList(params?: {
__authError?: boolean;
}> {
const result = await executeServerAction({
- url: buildApiUrl('/api/v1/quality/documents/available-orders', { q: params?.q }),
+ url: buildApiUrl('/api/v1/quality/documents/available-orders', {
+ q: params?.q,
+ client_id: params?.clientId ?? undefined,
+ item_id: params?.itemId ?? undefined,
+ }),
errorMessage: '수주 선택 목록 조회에 실패했습니다.',
});
@@ -660,6 +722,12 @@ export async function getOrderSelectList(params?: {
i.orderNumber.toLowerCase().includes(q) || i.siteName.toLowerCase().includes(q)
);
}
+ if (params?.clientId) {
+ filtered = filtered.filter(i => i.clientId === params.clientId);
+ }
+ if (params?.itemId) {
+ filtered = filtered.filter(i => i.itemId === params.itemId);
+ }
return { success: true, data: filtered };
}
return { success: false, data: [], error: result.error, __authError: result.__authError };
@@ -672,9 +740,19 @@ export async function getOrderSelectList(params?: {
id: String(item.id),
orderNumber: item.order_number,
siteName: item.site_name,
+ clientId: item.client_id ?? null,
clientName: item.client_name ?? '',
+ itemId: item.item_id ?? null,
+ itemName: item.item_name ?? '',
deliveryDate: item.delivery_date,
locationCount: item.location_count,
+ locations: (item.locations || []).map((loc) => ({
+ nodeId: loc.node_id,
+ floor: loc.floor,
+ symbol: loc.symbol,
+ orderWidth: loc.order_width,
+ orderHeight: loc.order_height,
+ })),
})),
};
}
diff --git a/src/components/quality/InspectionManagement/documents/InspectionReportModal.tsx b/src/components/quality/InspectionManagement/documents/InspectionReportModal.tsx
index dcd83d6b..68fe8793 100644
--- a/src/components/quality/InspectionManagement/documents/InspectionReportModal.tsx
+++ b/src/components/quality/InspectionManagement/documents/InspectionReportModal.tsx
@@ -36,8 +36,6 @@ interface InspectionReportModalProps {
inspection?: ProductInspection | null;
/** 페이지네이션용: orderItems (수정 모드에서는 formData.orderItems) */
orderItems?: OrderSettingItem[];
- /** FQC 문서 ID 매핑 (orderItemId → documentId) */
- fqcDocumentMap?: Record;
}
export function InspectionReportModal({
@@ -46,7 +44,6 @@ export function InspectionReportModal({
data,
inspection,
orderItems,
- fqcDocumentMap,
}: InspectionReportModalProps) {
const [currentPage, setCurrentPage] = useState(1);
const [inputPage, setInputPage] = useState('1');
@@ -61,8 +58,8 @@ export function InspectionReportModal({
const [fqcError, setFqcError] = useState(null);
const [templateLoadFailed, setTemplateLoadFailed] = useState(false);
- // FQC 모드 우선 (fqcDocumentMap 없어도 시도, template 로드 실패 시 fallback)
- const hasFqcDocuments = !!fqcDocumentMap && Object.keys(fqcDocumentMap).length > 0;
+ // FQC 모드 우선 (orderItems에 documentId가 있으면 FQC 문서 존재)
+ const hasFqcDocuments = !!orderItems && orderItems.some((i) => i.documentId);
const useFqcMode = !templateLoadFailed && (hasFqcDocuments || !!fqcTemplate);
// 총 페이지 수
@@ -96,12 +93,12 @@ export function InspectionReportModal({
// 페이지 변경 시 FQC 문서 로드
useEffect(() => {
- if (!open || !hasFqcDocuments || !orderItems || !fqcDocumentMap) return;
+ if (!open || !hasFqcDocuments || !orderItems) return;
const currentItem = orderItems[currentPage - 1];
if (!currentItem) return;
- const documentId = fqcDocumentMap[currentItem.id];
+ const documentId = currentItem.documentId;
if (!documentId) {
setFqcDocument(null);
setFqcError(null);
@@ -121,7 +118,7 @@ export function InspectionReportModal({
}
})
.finally(() => setIsLoadingFqc(false));
- }, [open, useFqcMode, currentPage, orderItems, fqcDocumentMap]);
+ }, [open, useFqcMode, currentPage, orderItems]);
// 기존 모드: 현재 페이지 문서 데이터 (fallback)
const legacyCurrentData = useMemo(() => {
diff --git a/src/components/quality/InspectionManagement/mockData.ts b/src/components/quality/InspectionManagement/mockData.ts
index bfad9d0c..c6a9dea7 100644
--- a/src/components/quality/InspectionManagement/mockData.ts
+++ b/src/components/quality/InspectionManagement/mockData.ts
@@ -62,13 +62,32 @@ const defaultSupervisor = {
// ===== Mock 수주 선택 목록 (모달용) =====
export const mockOrderSelectItems: OrderSelectItem[] = [
- { id: 'os-1', orderNumber: '123123', siteName: '현장명', deliveryDate: '2026-01-01', locationCount: 3 },
- { id: 'os-2', orderNumber: '123123', siteName: '현장명', deliveryDate: '2026-01-01', locationCount: 3 },
- { id: 'os-3', orderNumber: '123123', siteName: '현장명', deliveryDate: '2026-01-01', locationCount: 3 },
- { id: 'os-4', orderNumber: '123123', siteName: '현장명', deliveryDate: '2026-01-01', locationCount: 3 },
- { id: 'os-5', orderNumber: '123123', siteName: '현장명', deliveryDate: '2026-01-01', locationCount: 3 },
- { id: 'os-6', orderNumber: '123123', siteName: '현장명', deliveryDate: '2026-01-01', locationCount: 3 },
- { id: 'os-7', orderNumber: '123123', siteName: '현장명', deliveryDate: '2026-01-01', locationCount: 3 },
+ { id: 'os-1', orderNumber: '123123', siteName: '현장명', clientId: 1, clientName: '발주처A', itemId: 10, itemName: '방화셔터', deliveryDate: '2026-01-01', locationCount: 3, locations: [
+ { nodeId: 101, floor: '1층', symbol: 'A', orderWidth: 4100, orderHeight: 2700 },
+ { nodeId: 102, floor: '2층', symbol: 'B', orderWidth: 3800, orderHeight: 2500 },
+ { nodeId: 103, floor: '3층', symbol: 'C', orderWidth: 4200, orderHeight: 2800 },
+ ] },
+ { id: 'os-2', orderNumber: '123124', siteName: '현장명', clientId: 1, clientName: '발주처A', itemId: 10, itemName: '방화셔터', deliveryDate: '2026-01-01', locationCount: 2, locations: [
+ { nodeId: 201, floor: '1층', symbol: 'D', orderWidth: 3500, orderHeight: 2400 },
+ { nodeId: 202, floor: '2층', symbol: 'E', orderWidth: 3600, orderHeight: 2500 },
+ ] },
+ { id: 'os-3', orderNumber: '123125', siteName: '현장명', clientId: 1, clientName: '발주처A', itemId: 20, itemName: '스크린', deliveryDate: '2026-01-01', locationCount: 1, locations: [
+ { nodeId: 301, floor: '1층', symbol: 'F', orderWidth: 5000, orderHeight: 3000 },
+ ] },
+ { id: 'os-4', orderNumber: '123126', siteName: '현장명', clientId: 2, clientName: '발주처B', itemId: 10, itemName: '방화셔터', deliveryDate: '2026-01-01', locationCount: 2, locations: [
+ { nodeId: 401, floor: '1층', symbol: 'G', orderWidth: 4000, orderHeight: 2600 },
+ { nodeId: 402, floor: '2층', symbol: 'H', orderWidth: 4100, orderHeight: 2700 },
+ ] },
+ { id: 'os-5', orderNumber: '123127', siteName: '현장명', clientId: 2, clientName: '발주처B', itemId: 10, itemName: '방화셔터', deliveryDate: '2026-01-01', locationCount: 1, locations: [
+ { nodeId: 501, floor: '1층', symbol: 'I', orderWidth: 3900, orderHeight: 2500 },
+ ] },
+ { id: 'os-6', orderNumber: '123128', siteName: '현장명', clientId: 3, clientName: '발주처C', itemId: 30, itemName: '절곡물', deliveryDate: '2026-01-01', locationCount: 2, locations: [
+ { nodeId: 601, floor: '1층', symbol: 'J', orderWidth: 2000, orderHeight: 1500 },
+ { nodeId: 602, floor: '2층', symbol: 'K', orderWidth: 2100, orderHeight: 1600 },
+ ] },
+ { id: 'os-7', orderNumber: '123129', siteName: '현장명', clientId: 3, clientName: '발주처C', itemId: 30, itemName: '절곡물', deliveryDate: '2026-01-01', locationCount: 1, locations: [
+ { nodeId: 701, floor: '1층', symbol: 'L', orderWidth: 2200, orderHeight: 1700 },
+ ] },
];
// ===== Mock 수주 설정 항목 =====
diff --git a/src/components/quality/InspectionManagement/types.ts b/src/components/quality/InspectionManagement/types.ts
index 47fd1748..3c69238c 100644
--- a/src/components/quality/InspectionManagement/types.ts
+++ b/src/components/quality/InspectionManagement/types.ts
@@ -61,6 +61,10 @@ export interface OrderSettingItem {
orderId?: number; // 수주 DB ID (FQC 문서 연동용)
orderNumber: string; // 수주번호
siteName: string; // 현장명
+ clientId?: number | null; // 발주처 ID
+ clientName?: string; // 발주처명
+ itemId?: number | null; // 품목(모델) ID
+ itemName?: string; // 품목(모델)명
deliveryDate: string; // 납품일
floor: string; // 층수
symbol: string; // 부호
@@ -69,6 +73,8 @@ export interface OrderSettingItem {
constructionWidth: number; // 시공 규격 - 가로
constructionHeight: number; // 시공 규격 - 세로
changeReason: string; // 변경사유
+ // FQC 성적서 EAV 문서 ID (quality_document_locations.document_id)
+ documentId?: number | null;
// 검사 결과 데이터
inspectionData?: ProductInspectionData;
}
@@ -120,9 +126,22 @@ export interface OrderSelectItem {
id: string;
orderNumber: string; // 수주번호
siteName: string; // 현장명
+ clientId: number | null; // 발주처 ID
clientName: string; // 발주처
+ itemId: number | null; // 품목(모델) ID
+ itemName: string; // 품목(모델)명
deliveryDate: string; // 납품일
locationCount: number; // 개소
+ locations: OrderSelectLocation[]; // 개소 상세
+}
+
+// 수주의 개소(root node) 정보
+export interface OrderSelectLocation {
+ nodeId: number;
+ floor: string;
+ symbol: string;
+ orderWidth: number;
+ orderHeight: number;
}
// ===== 메인 데이터 =====