From 2a7066b6fd6feddbcf4d86a88aea1c6f3332c11a Mon Sep 17 00:00:00 2001 From: hjjeong Date: Mon, 5 Jan 2026 17:08:03 +0900 Subject: [PATCH 1/3] =?UTF-8?q?=ED=85=8C=EC=9D=B4=EB=B8=94=EC=97=90=20?= =?UTF-8?q?=EC=A1=B4=EC=9E=AC=ED=95=98=EB=8A=94=20=EC=BB=AC=EB=9F=BC?= =?UTF-8?q?=EB=A7=8C=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend-node/src/services/tableManagementService.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/backend-node/src/services/tableManagementService.ts b/backend-node/src/services/tableManagementService.ts index def9a978..8ac5989b 100644 --- a/backend-node/src/services/tableManagementService.ts +++ b/backend-node/src/services/tableManagementService.ts @@ -2409,11 +2409,19 @@ export class TableManagementService { } // SET 절 생성 (수정할 데이터) - 먼저 생성 + // 🔧 테이블에 존재하는 컬럼만 UPDATE (가상 컬럼 제외) const setConditions: string[] = []; const setValues: any[] = []; let paramIndex = 1; + const skippedColumns: string[] = []; Object.keys(updatedData).forEach((column) => { + // 테이블에 존재하지 않는 컬럼은 스킵 + if (!columnTypeMap.has(column)) { + skippedColumns.push(column); + return; + } + const dataType = columnTypeMap.get(column) || "text"; setConditions.push( `"${column}" = $${paramIndex}::${this.getPostgreSQLType(dataType)}` @@ -2424,6 +2432,10 @@ export class TableManagementService { paramIndex++; }); + if (skippedColumns.length > 0) { + logger.info(`⚠️ 테이블에 존재하지 않는 컬럼 스킵: ${skippedColumns.join(", ")}`); + } + // WHERE 조건 생성 (PRIMARY KEY 우선, 없으면 모든 원본 데이터 사용) let whereConditions: string[] = []; let whereValues: any[] = []; From 714698c20f6e7109c096563ae1275cc4dc1f6568 Mon Sep 17 00:00:00 2001 From: hjjeong Date: Mon, 5 Jan 2026 17:08:47 +0900 Subject: [PATCH 2/3] =?UTF-8?q?=EA=B5=AC=EB=A7=A4=EA=B4=80=EB=A6=AC=5F?= =?UTF-8?q?=EB=B0=9C=EC=A3=BC=EA=B4=80=EB=A6=AC=20=EC=A0=80=EC=9E=A5=20?= =?UTF-8?q?=EC=8B=9C=20=EB=A7=88=EC=8A=A4=ED=84=B0=EC=A0=95=EB=B3=B4=20?= =?UTF-8?q?=ED=92=88=EB=AA=A9=EC=97=90=20=EC=A0=84=EB=8B=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/lib/utils/buttonActions.ts | 34 +++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/frontend/lib/utils/buttonActions.ts b/frontend/lib/utils/buttonActions.ts index 5587fc1a..681e9a3f 100644 --- a/frontend/lib/utils/buttonActions.ts +++ b/frontend/lib/utils/buttonActions.ts @@ -995,6 +995,40 @@ export class ButtonActionExecutor { console.log("📋 [handleSave] 범용 폼 모달 공통 필드:", commonFields); } + // 🆕 루트 레벨 formData에서 RepeaterFieldGroup에 전달할 공통 필드 추출 + // 주문번호, 발주번호 등 마스터-디테일 관계에서 필요한 필드만 명시적으로 지정 + const masterDetailFields = [ + // 번호 필드 + "order_no", // 발주번호 + "sales_order_no", // 수주번호 + "shipment_no", // 출하번호 + "receipt_no", // 입고번호 + "work_order_no", // 작업지시번호 + // 거래처 필드 + "supplier_code", // 공급처 코드 + "supplier_name", // 공급처 이름 + "customer_code", // 고객 코드 + "customer_name", // 고객 이름 + // 날짜 필드 + "order_date", // 발주일 + "sales_date", // 수주일 + "shipment_date", // 출하일 + "receipt_date", // 입고일 + "due_date", // 납기일 + // 담당자/메모 필드 + "manager", // 담당자 + "memo", // 메모 + "remark", // 비고 + ]; + + for (const fieldName of masterDetailFields) { + const value = context.formData[fieldName]; + if (value !== undefined && value !== "" && value !== null && !(fieldName in commonFields)) { + commonFields[fieldName] = value; + } + } + console.log("📋 [handleSave] 최종 공통 필드 (마스터-디테일 필드 포함):", commonFields); + for (const item of parsedData) { // 메타 필드 제거 (eslint 경고 무시 - 의도적으로 분리) From 64105bf525644f6c76f8f87b8ba3ea1d7acd0788 Mon Sep 17 00:00:00 2001 From: hjjeong Date: Mon, 5 Jan 2026 18:21:29 +0900 Subject: [PATCH 3/3] =?UTF-8?q?=EB=B0=9C=EC=A3=BC=EA=B4=80=EB=A6=AC=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=EC=8B=9C=20=EA=B3=B5=EA=B8=89=EC=B2=98=20?= =?UTF-8?q?=ED=91=9C=EC=8B=9C=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AutocompleteSearchInputComponent.tsx | 145 +++++++++++++++++- 1 file changed, 141 insertions(+), 4 deletions(-) diff --git a/frontend/lib/registry/components/autocomplete-search-input/AutocompleteSearchInputComponent.tsx b/frontend/lib/registry/components/autocomplete-search-input/AutocompleteSearchInputComponent.tsx index 7a115ea3..cbd2744c 100644 --- a/frontend/lib/registry/components/autocomplete-search-input/AutocompleteSearchInputComponent.tsx +++ b/frontend/lib/registry/components/autocomplete-search-input/AutocompleteSearchInputComponent.tsx @@ -44,7 +44,42 @@ export function AutocompleteSearchInputComponent({ const displayField = config?.displayField || propDisplayField || ""; const displayFields = config?.displayFields || (displayField ? [displayField] : []); // 다중 표시 필드 const displaySeparator = config?.displaySeparator || " → "; // 구분자 - const valueField = config?.valueField || propValueField || ""; + + // valueField 결정: fieldMappings 기반으로 추론 (config.valueField가 fieldMappings에 없으면 무시) + const getValueField = () => { + // fieldMappings가 있으면 그 안에서 추론 (가장 신뢰할 수 있는 소스) + if (config?.fieldMappings && config.fieldMappings.length > 0) { + // config.valueField가 fieldMappings의 sourceField에 있으면 사용 + if (config?.valueField) { + const hasValueFieldInMappings = config.fieldMappings.some( + (m: any) => m.sourceField === config.valueField + ); + if (hasValueFieldInMappings) { + return config.valueField; + } + // fieldMappings에 없으면 무시하고 추론 + } + + // _code 또는 _id로 끝나는 필드 우선 (보통 PK나 코드 필드) + const codeMapping = config.fieldMappings.find( + (m: any) => m.sourceField?.endsWith("_code") || m.sourceField?.endsWith("_id") + ); + if (codeMapping) { + return codeMapping.sourceField; + } + + // 없으면 첫 번째 매핑 사용 + return config.fieldMappings[0].sourceField || ""; + } + + // fieldMappings가 없으면 기존 방식 + if (config?.valueField) return config.valueField; + if (propValueField) return propValueField; + + return ""; + }; + const valueField = getValueField(); + const searchFields = config?.searchFields || propSearchFields || displayFields; // 검색 필드도 다중 표시 필드 사용 const placeholder = config?.placeholder || propPlaceholder || "검색..."; @@ -76,11 +111,39 @@ export function AutocompleteSearchInputComponent({ // 선택된 데이터를 ref로도 유지 (리렌더링 시 초기화 방지) const selectedDataRef = useRef(null); const inputValueRef = useRef(""); + const initialValueLoadedRef = useRef(null); // 초기값 로드 추적 // formData에서 현재 값 가져오기 (isInteractive 모드) - const currentValue = isInteractive && formData && component?.columnName - ? formData[component.columnName] - : value; + // 우선순위: 1) component.columnName, 2) fieldMappings에서 valueField에 매핑된 targetField + const getCurrentValue = () => { + if (!isInteractive || !formData) { + return value; + } + + // 1. component.columnName으로 직접 바인딩된 경우 + if (component?.columnName && formData[component.columnName] !== undefined) { + return formData[component.columnName]; + } + + // 2. fieldMappings에서 valueField와 매핑된 targetField에서 값 가져오기 + if (config?.fieldMappings && Array.isArray(config.fieldMappings)) { + const valueFieldMapping = config.fieldMappings.find( + (mapping: any) => mapping.sourceField === valueField + ); + + if (valueFieldMapping) { + const targetField = valueFieldMapping.targetField || valueFieldMapping.targetColumn; + + if (targetField && formData[targetField] !== undefined) { + return formData[targetField]; + } + } + } + + return value; + }; + + const currentValue = getCurrentValue(); // selectedData 변경 시 ref도 업데이트 useEffect(() => { @@ -98,6 +161,79 @@ export function AutocompleteSearchInputComponent({ } }, []); + // 초기값이 있을 때 해당 값의 표시 텍스트를 조회하여 설정 + useEffect(() => { + const loadInitialDisplayValue = async () => { + // 이미 로드된 값이거나, 값이 없거나, 이미 선택된 데이터가 있으면 스킵 + if (!currentValue || selectedData || selectedDataRef.current) { + return; + } + + // 이미 같은 값을 로드한 적이 있으면 스킵 + if (initialValueLoadedRef.current === currentValue) { + return; + } + + // 테이블명과 필드 정보가 없으면 스킵 + if (!tableName || !valueField) { + return; + } + + console.log("🔄 AutocompleteSearchInput 초기값 로드:", { + currentValue, + tableName, + valueField, + displayFields, + }); + + try { + // API를 통해 해당 값의 표시 텍스트 조회 + const { apiClient } = await import("@/lib/api/client"); + const filterConditionWithValue = { + ...filterCondition, + [valueField]: currentValue, + }; + + const params = new URLSearchParams({ + searchText: "", + searchFields: searchFields.join(","), + filterCondition: JSON.stringify(filterConditionWithValue), + page: "1", + limit: "10", + }); + + const response = await apiClient.get<{ success: boolean; data: EntitySearchResult[] }>( + `/entity-search/${tableName}?${params.toString()}` + ); + + if (response.data.success && response.data.data && response.data.data.length > 0) { + const matchedItem = response.data.data.find((item: EntitySearchResult) => + String(item[valueField]) === String(currentValue) + ); + + if (matchedItem) { + const displayText = getDisplayValue(matchedItem); + console.log("✅ 초기값 표시 텍스트 로드 성공:", { + currentValue, + displayText, + matchedItem, + }); + + setSelectedData(matchedItem); + setInputValue(displayText); + selectedDataRef.current = matchedItem; + inputValueRef.current = displayText; + initialValueLoadedRef.current = currentValue; + } + } + } catch (error) { + console.error("❌ 초기값 표시 텍스트 로드 실패:", error); + } + }; + + loadInitialDisplayValue(); + }, [currentValue, tableName, valueField, displayFields, filterCondition, searchFields, selectedData]); + // value가 변경되면 표시값 업데이트 - 단, selectedData가 있으면 유지 useEffect(() => { // selectedData가 있으면 표시값 유지 (사용자가 방금 선택한 경우) @@ -107,6 +243,7 @@ export function AutocompleteSearchInputComponent({ if (!currentValue) { setInputValue(""); + initialValueLoadedRef.current = null; // 값이 없어지면 초기화 } }, [currentValue, selectedData]);