From 109380b9e5ea1b19936c0cb2fce18b51518f9d59 Mon Sep 17 00:00:00 2001 From: leeheejin Date: Mon, 15 Dec 2025 17:01:04 +0900 Subject: [PATCH 1/7] =?UTF-8?q?=EC=9D=B4=EC=A0=9C=20=EB=94=94=EB=B9=84?= =?UTF-8?q?=EC=97=90=20=ED=95=9C=EA=B8=80=EB=A1=9C=20=EC=B6=9C=EB=B0=9C?= =?UTF-8?q?=EC=A7=80=20=EB=AA=A9=EC=A0=81=EC=A7=80=20=EC=A0=80=EC=9E=A5?= =?UTF-8?q?=EB=90=98=EB=8F=84=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../LocationSwapSelectorComponent.tsx | 6 +++--- .../lib/registry/components/location-swap-selector/index.ts | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/frontend/lib/registry/components/location-swap-selector/LocationSwapSelectorComponent.tsx b/frontend/lib/registry/components/location-swap-selector/LocationSwapSelectorComponent.tsx index 7a693ad5..88e9002a 100644 --- a/frontend/lib/registry/components/location-swap-selector/LocationSwapSelectorComponent.tsx +++ b/frontend/lib/registry/components/location-swap-selector/LocationSwapSelectorComponent.tsx @@ -107,10 +107,10 @@ export function LocationSwapSelectorComponent(props: LocationSwapSelectorProps) const dbTableName = config.dbTableName || "vehicles"; const dbKeyField = config.dbKeyField || "user_id"; - // 기본 옵션 (포항/광양) + // 기본 옵션 (포항/광양) - 한글로 저장 const DEFAULT_OPTIONS: LocationOption[] = [ - { value: "pohang", label: "포항" }, - { value: "gwangyang", label: "광양" }, + { value: "포항", label: "포항" }, + { value: "광양", label: "광양" }, ]; // 상태 diff --git a/frontend/lib/registry/components/location-swap-selector/index.ts b/frontend/lib/registry/components/location-swap-selector/index.ts index c4c30418..7f7447cf 100644 --- a/frontend/lib/registry/components/location-swap-selector/index.ts +++ b/frontend/lib/registry/components/location-swap-selector/index.ts @@ -26,9 +26,9 @@ export const LocationSwapSelectorDefinition = createComponentDefinition({ labelField: "location_name", // 표시 필드 codeCategory: "", // 코드 관리 카테고리 (type이 "code"일 때) staticOptions: [ - { value: "pohang", label: "포항" }, - { value: "gwangyang", label: "광양" }, - ], // 정적 옵션 (type이 "static"일 때) + { value: "포항", label: "포항" }, + { value: "광양", label: "광양" }, + ], // 정적 옵션 (type이 "static"일 때) - 한글로 저장 }, // 필드 매핑 departureField: "departure", // 출발지 저장 필드 From f0322a49addf778bd1f228fde2e6d9bcce852817 Mon Sep 17 00:00:00 2001 From: kjs Date: Tue, 6 Jan 2026 14:24:30 +0900 Subject: [PATCH 2/7] =?UTF-8?q?=EB=B2=94=EC=9A=A9=20=ED=8F=BC=EB=AA=A8?= =?UTF-8?q?=EB=8B=AC=20=EB=9D=BC=EB=B2=A8=EB=A1=9C=20=EB=9C=A8=EA=B2=8C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TableSectionRenderer.tsx | 48 ++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/frontend/lib/registry/components/universal-form-modal/TableSectionRenderer.tsx b/frontend/lib/registry/components/universal-form-modal/TableSectionRenderer.tsx index 9c238b47..4f872bc1 100644 --- a/frontend/lib/registry/components/universal-form-modal/TableSectionRenderer.tsx +++ b/frontend/lib/registry/components/universal-form-modal/TableSectionRenderer.tsx @@ -385,6 +385,9 @@ export function TableSectionRenderer({ // 소스 테이블의 카테고리 타입 컬럼 목록 const [sourceCategoryColumns, setSourceCategoryColumns] = useState([]); + // 소스 테이블의 컬럼 라벨 (API에서 동적 로드) + const [sourceColumnLabels, setSourceColumnLabels] = useState>({}); + // 소스 테이블의 카테고리 타입 컬럼 목록 로드 useEffect(() => { const loadCategoryColumns = async () => { @@ -410,6 +413,44 @@ export function TableSectionRenderer({ loadCategoryColumns(); }, [tableConfig.source.tableName]); + // 소스 테이블의 컬럼 라벨 로드 (source.columnLabels가 비어있을 때만) + useEffect(() => { + const loadColumnLabels = async () => { + const sourceTableName = tableConfig.source.tableName; + if (!sourceTableName) return; + + // 이미 source.columnLabels가 설정되어 있으면 스킵 + if (tableConfig.source.columnLabels && Object.keys(tableConfig.source.columnLabels).length > 0) { + return; + } + + try { + const response = await apiClient.get(`/table-management/tables/${sourceTableName}/columns`); + + if (response.data?.success && response.data.data) { + const columnsData = response.data.data.columns || response.data.data || []; + const labels: Record = {}; + + for (const col of columnsData) { + const colName = col.column_name || col.columnName; + // displayName: API에서 반환하는 라벨 (COALESCE(cl.column_label, c.column_name)) + const colLabel = col.displayName || col.column_label || col.columnLabel || col.comment; + // 라벨이 컬럼명과 다를 때만 저장 (의미있는 라벨인 경우) + if (colName && colLabel && colLabel !== colName) { + labels[colName] = colLabel; + } + } + + setSourceColumnLabels(labels); + } + } catch (error) { + console.error("소스 테이블 컬럼 라벨 조회 실패:", error); + } + }; + + loadColumnLabels(); + }, [tableConfig.source.tableName, tableConfig.source.columnLabels]); + // 조건부 테이블: 동적 옵션 로드 (optionSource 설정이 있는 경우) useEffect(() => { if (!isConditionalMode) return; @@ -1305,7 +1346,12 @@ export function TableSectionRenderer({ const sourceTable = source.tableName; const sourceColumns = source.displayColumns; const sourceSearchFields = source.searchColumns; - const columnLabels = source.columnLabels || {}; + // 컬럼 라벨: source.columnLabels가 있으면 우선 사용, 없으면 동적 로드된 라벨 사용 + const columnLabels = useMemo(() => { + const configLabels = source.columnLabels || {}; + // 설정된 라벨이 있으면 설정 우선, 없으면 API에서 로드한 라벨 사용 + return { ...sourceColumnLabels, ...configLabels }; + }, [source.columnLabels, sourceColumnLabels]); const modalTitle = uiConfig?.modalTitle || "항목 검색 및 선택"; const multiSelect = uiConfig?.multiSelect ?? true; From 6ae0778b4cc8cf46c8940e33005954c2eb90c3a4 Mon Sep 17 00:00:00 2001 From: leeheejin Date: Tue, 6 Jan 2026 14:43:57 +0900 Subject: [PATCH 3/7] =?UTF-8?q?=ED=95=84=ED=84=B0=EC=9D=98=20=EB=9D=BC?= =?UTF-8?q?=EB=B2=A8=EB=8F=84=20=EC=BD=94=EB=93=9C=EB=A7=90=EA=B3=A0=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=ED=95=9C=EA=B1=B8=EB=A1=9C=20=EB=82=98?= =?UTF-8?q?=EC=98=A4=EA=B2=8C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../table-list/TableListComponent.tsx | 38 ++++++++++++++----- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/frontend/lib/registry/components/table-list/TableListComponent.tsx b/frontend/lib/registry/components/table-list/TableListComponent.tsx index 7ac521af..5f3f7c8d 100644 --- a/frontend/lib/registry/components/table-list/TableListComponent.tsx +++ b/frontend/lib/registry/components/table-list/TableListComponent.tsx @@ -2250,9 +2250,9 @@ export const TableListComponent: React.FC = ({ // 🆕 편집 모드 진입 placeholder (실제 구현은 visibleColumns 정의 후) const startEditingRef = useRef<() => void>(() => {}); - // 🆕 각 컬럼의 고유값 목록 계산 + // 🆕 각 컬럼의 고유값 목록 계산 (라벨 포함) const columnUniqueValues = useMemo(() => { - const result: Record = {}; + const result: Record> = {}; if (data.length === 0) return result; @@ -2260,16 +2260,34 @@ export const TableListComponent: React.FC = ({ if (column.columnName === "__checkbox__") return; const mappedColumnName = joinColumnMapping[column.columnName] || column.columnName; - const values = new Set(); + // 라벨 컬럼 후보들 (백엔드에서 _name, _label, _value_label 등으로 반환할 수 있음) + const labelColumnCandidates = [ + `${column.columnName}_name`, // 예: division_name + `${column.columnName}_label`, // 예: division_label + `${column.columnName}_value_label`, // 예: division_value_label + ]; + const valuesMap = new Map(); // value -> label data.forEach((row) => { const val = row[mappedColumnName]; if (val !== null && val !== undefined && val !== "") { - values.add(String(val)); + const valueStr = String(val); + // 라벨 컬럼 후보들 중 값이 있는 것 사용, 없으면 원본 값 사용 + let label = valueStr; + for (const labelCol of labelColumnCandidates) { + if (row[labelCol] && row[labelCol] !== "") { + label = String(row[labelCol]); + break; + } + } + valuesMap.set(valueStr, label); } }); - result[column.columnName] = Array.from(values).sort(); + // value-label 쌍으로 저장하고 라벨 기준 정렬 + result[column.columnName] = Array.from(valuesMap.entries()) + .map(([value, label]) => ({ value, label })) + .sort((a, b) => a.label.localeCompare(b.label)); }); return result; @@ -5758,16 +5776,16 @@ export const TableListComponent: React.FC = ({ )}
- {columnUniqueValues[column.columnName]?.slice(0, 50).map((val) => { - const isSelected = headerFilters[column.columnName]?.has(val); + {columnUniqueValues[column.columnName]?.slice(0, 50).map((item) => { + const isSelected = headerFilters[column.columnName]?.has(item.value); return (
toggleHeaderFilter(column.columnName, val)} + onClick={() => toggleHeaderFilter(column.columnName, item.value)} >
= ({ > {isSelected && }
- {val || "(빈 값)"} + {item.label || "(빈 값)"}
); })} From e08c50c771a5de38cd9313373a8a3a3750b0154c Mon Sep 17 00:00:00 2001 From: hjjeong Date: Tue, 6 Jan 2026 15:01:50 +0900 Subject: [PATCH 4/7] =?UTF-8?q?=EA=B1=B0=EB=9E=98=EC=B2=98=20=ED=92=88?= =?UTF-8?q?=EB=AA=A9=EC=A0=95=EB=B3=B4=20=EA=B1=B0=EB=9E=98=EC=B2=98?= =?UTF-8?q?=ED=92=88=EB=B2=88/=EB=8B=A8=EA=B0=80=20=EC=9E=85=EB=A0=A5=20?= =?UTF-8?q?=EC=97=86=EC=9D=B4=20=EC=A0=80=EC=9E=A5=EB=90=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SelectedItemsDetailInputComponent.tsx | 20 +++++++++++++++++-- frontend/lib/utils/buttonActions.ts | 15 +++++++++++++- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/frontend/lib/registry/components/selected-items-detail-input/SelectedItemsDetailInputComponent.tsx b/frontend/lib/registry/components/selected-items-detail-input/SelectedItemsDetailInputComponent.tsx index 925ca174..e91d34f4 100644 --- a/frontend/lib/registry/components/selected-items-detail-input/SelectedItemsDetailInputComponent.tsx +++ b/frontend/lib/registry/components/selected-items-detail-input/SelectedItemsDetailInputComponent.tsx @@ -432,10 +432,25 @@ export const SelectedItemsDetailInputComponent: React.FC { + itemsList.forEach((item, itemIndex) => { // 각 그룹의 엔트리 배열들을 준비 const groupEntriesArrays: GroupEntry[][] = groups.map((group) => item.fieldGroups[group.id] || []); + // 🆕 모든 그룹이 비어있는지 확인 + const allGroupsEmpty = groupEntriesArrays.every((arr) => arr.length === 0); + + if (allGroupsEmpty) { + // 🆕 모든 그룹이 비어있으면 품목 기본 정보만으로 레코드 생성 + // (거래처 품번/품명, 기간별 단가 없이도 저장 가능) + console.log("📝 [generateCartesianProduct] 모든 그룹이 비어있음 - 품목 기본 레코드 생성", { + itemIndex, + itemId: item.id, + }); + // 빈 객체를 추가하면 parentKeys와 합쳐져서 기본 레코드가 됨 + allRecords.push({}); + return; + } + // Cartesian Product 재귀 함수 const cartesian = (arrays: GroupEntry[][], currentIndex: number, currentCombination: Record) => { if (currentIndex === arrays.length) { @@ -446,7 +461,8 @@ export const SelectedItemsDetailInputComponent: React.FC g.entries); - const combinations = cartesianProduct(entryArrays); + + // 🆕 모든 그룹이 비어있는지 확인 + const allGroupsEmpty = entryArrays.every((arr) => arr.length === 0); + + let combinations: any[][]; + if (allGroupsEmpty) { + // 🆕 모든 그룹이 비어있으면 빈 조합 하나 생성 (품목 기본 정보만으로 저장) + console.log("📝 [handleBatchSave] 모든 그룹이 비어있음 - 기본 레코드 생성"); + combinations = [[]]; + } else { + // 빈 그룹을 필터링하여 카티션 곱 계산 (빈 그룹은 무시) + const nonEmptyArrays = entryArrays.filter((arr) => arr.length > 0); + combinations = nonEmptyArrays.length > 0 ? cartesianProduct(nonEmptyArrays) : [[]]; + } // 각 조합을 개별 레코드로 저장 for (let i = 0; i < combinations.length; i++) { From 77bb917248bb85edb79ee00efa7de66554e0ed4b Mon Sep 17 00:00:00 2001 From: SeongHyun Kim Date: Tue, 6 Jan 2026 15:03:22 +0900 Subject: [PATCH 5/7] =?UTF-8?q?feat:=20RepeaterFieldGroup=20=EC=83=81?= =?UTF-8?q?=EC=9C=84=20=ED=8F=BC=20=ED=95=84=EB=93=9C=20=EC=A0=84=EB=8B=AC?= =?UTF-8?q?=20=EB=B0=A9=EC=8B=9D=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 하드코딩된 masterDetailFields 배열을 규칙 기반 필터링으로 변경 - 제외 규칙: comp_ 접두사, _numberingRuleId 접미사, 배열/객체 타입, 빈 값 등 - 새 필드 추가 시 코드 수정 불필요하도록 개선 - 에러 로깅 상세 정보 추가 (status, data, message, fullError) --- frontend/lib/utils/buttonActions.ts | 58 +++++++++++++---------------- 1 file changed, 26 insertions(+), 32 deletions(-) diff --git a/frontend/lib/utils/buttonActions.ts b/frontend/lib/utils/buttonActions.ts index 681e9a3f..472de705 100644 --- a/frontend/lib/utils/buttonActions.ts +++ b/frontend/lib/utils/buttonActions.ts @@ -996,38 +996,27 @@ export class ButtonActionExecutor { } // 🆕 루트 레벨 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, value] of Object.entries(context.formData)) { + // 제외 규칙 1: comp_로 시작하는 필드 (하위 항목 배열) + if (fieldName.startsWith("comp_")) continue; + // 제외 규칙 2: _numberingRuleId로 끝나는 필드 (채번 규칙 메타 정보) + if (fieldName.endsWith("_numberingRuleId")) continue; + // 제외 규칙 3: _로 시작하는 필드 (내부 메타 필드) + if (fieldName.startsWith("_")) continue; + // 제외 규칙 4: 배열 타입 (하위 항목 데이터) + if (Array.isArray(value)) continue; + // 제외 규칙 5: 객체 타입 (복잡한 구조 데이터) - null 제외 + if (value !== null && typeof value === "object") continue; + // 제외 규칙 6: 빈 값 + if (value === undefined || value === "" || value === null) continue; + // 제외 규칙 7: 이미 commonFields에 있는 필드 (범용 폼 모달에서 가져온 필드) + if (fieldName in commonFields) continue; - for (const fieldName of masterDetailFields) { - const value = context.formData[fieldName]; - if (value !== undefined && value !== "" && value !== null && !(fieldName in commonFields)) { - commonFields[fieldName] = value; - } + // 위 규칙에 해당하지 않는 단순 값(문자열, 숫자, 날짜 등)은 공통 필드로 전달 + commonFields[fieldName] = value; } - console.log("📋 [handleSave] 최종 공통 필드 (마스터-디테일 필드 포함):", commonFields); + console.log("📋 [handleSave] 최종 공통 필드 (규칙 기반 자동 추출):", commonFields); for (const item of parsedData) { // 메타 필드 제거 (eslint 경고 무시 - 의도적으로 분리) @@ -1089,10 +1078,15 @@ export class ButtonActionExecutor { console.log("✅ [handleSave] RepeaterFieldGroup UPDATE 완료:", updateResult.data); } } catch (err) { - const error = err as { response?: { data?: unknown }; message?: string }; + const error = err as { response?: { data?: unknown; status?: number }; message?: string }; console.error( `❌ [handleSave] RepeaterFieldGroup 저장 실패 (${repeaterTargetTable}):`, - error.response?.data || error.message, + { + status: error.response?.status, + data: error.response?.data, + message: error.message, + fullError: JSON.stringify(error.response?.data, null, 2), + }, ); } } From 25b7e637de6ec0fc23d6370c5c9ad06601a94ba2 Mon Sep 17 00:00:00 2001 From: SeongHyun Kim Date: Tue, 6 Jan 2026 15:29:26 +0900 Subject: [PATCH 6/7] =?UTF-8?q?feat:=20=ED=85=8C=EC=9D=B4=EB=B8=94=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=A0=80=EC=9E=A5=20=EC=8B=9C=20?= =?UTF-8?q?=EC=A1=B4=EC=9E=AC=ED=95=98=EC=A7=80=20=EC=95=8A=EB=8A=94=20?= =?UTF-8?q?=EC=BB=AC=EB=9F=BC=20=EC=9E=90=EB=8F=99=20=ED=95=84=ED=84=B0?= =?UTF-8?q?=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - tableManagementService.addTableData: 테이블 스키마 기반 컬럼 필터링 로직 추가 - 무시된 컬럼 정보를 API 응답에 포함 (skippedColumns, savedColumns) - 프론트엔드 콘솔에 무시된 컬럼 경고 출력 - conditional-container의 UI 제어용 필드(condition) 등으로 인한 저장 에러 방지 --- .../controllers/tableManagementController.ts | 16 +++++-- .../src/services/tableManagementService.ts | 48 +++++++++++++++++-- frontend/lib/utils/buttonActions.ts | 7 +++ 3 files changed, 63 insertions(+), 8 deletions(-) diff --git a/backend-node/src/controllers/tableManagementController.ts b/backend-node/src/controllers/tableManagementController.ts index 83384be6..9b3d81a2 100644 --- a/backend-node/src/controllers/tableManagementController.ts +++ b/backend-node/src/controllers/tableManagementController.ts @@ -901,13 +901,23 @@ export async function addTableData( } // 데이터 추가 - await tableManagementService.addTableData(tableName, data); + const result = await tableManagementService.addTableData(tableName, data); logger.info(`테이블 데이터 추가 완료: ${tableName}`); - const response: ApiResponse = { + // 무시된 컬럼이 있으면 경고 정보 포함 + const response: ApiResponse<{ + skippedColumns?: string[]; + savedColumns?: string[]; + }> = { success: true, - message: "테이블 데이터를 성공적으로 추가했습니다.", + message: result.skippedColumns.length > 0 + ? `테이블 데이터를 추가했습니다. (무시된 컬럼 ${result.skippedColumns.length}개: ${result.skippedColumns.join(", ")})` + : "테이블 데이터를 성공적으로 추가했습니다.", + data: { + skippedColumns: result.skippedColumns.length > 0 ? result.skippedColumns : undefined, + savedColumns: result.savedColumns, + }, }; res.status(201).json(response); diff --git a/backend-node/src/services/tableManagementService.ts b/backend-node/src/services/tableManagementService.ts index 8ac5989b..98db1eee 100644 --- a/backend-node/src/services/tableManagementService.ts +++ b/backend-node/src/services/tableManagementService.ts @@ -2261,11 +2261,12 @@ export class TableManagementService { /** * 테이블에 데이터 추가 + * @returns 무시된 컬럼 정보 (디버깅용) */ async addTableData( tableName: string, data: Record - ): Promise { + ): Promise<{ skippedColumns: string[]; savedColumns: string[] }> { try { logger.info(`=== 테이블 데이터 추가 시작: ${tableName} ===`); logger.info(`추가할 데이터:`, data); @@ -2296,10 +2297,41 @@ export class TableManagementService { logger.info(`created_date 자동 추가: ${data.created_date}`); } - // 컬럼명과 값을 분리하고 타입에 맞게 변환 - const columns = Object.keys(data); - const values = Object.values(data).map((value, index) => { - const columnName = columns[index]; + // 🆕 테이블에 존재하는 컬럼만 필터링 (존재하지 않는 컬럼은 무시) + const skippedColumns: string[] = []; + const existingColumns = Object.keys(data).filter((col) => { + const exists = columnTypeMap.has(col); + if (!exists) { + skippedColumns.push(col); + } + return exists; + }); + + // 무시된 컬럼이 있으면 경고 로그 출력 + if (skippedColumns.length > 0) { + logger.warn( + `⚠️ [${tableName}] 테이블에 존재하지 않는 컬럼 ${skippedColumns.length}개 무시됨: ${skippedColumns.join(", ")}` + ); + logger.warn( + `⚠️ [${tableName}] 무시된 컬럼 상세:`, + skippedColumns.map((col) => ({ column: col, value: data[col] })) + ); + } + + if (existingColumns.length === 0) { + throw new Error( + `저장할 유효한 컬럼이 없습니다. 테이블: ${tableName}, 전달된 컬럼: ${Object.keys(data).join(", ")}` + ); + } + + logger.info( + `✅ [${tableName}] 저장될 컬럼 ${existingColumns.length}개: ${existingColumns.join(", ")}` + ); + + // 컬럼명과 값을 분리하고 타입에 맞게 변환 (존재하는 컬럼만) + const columns = existingColumns; + const values = columns.map((columnName) => { + const value = data[columnName]; const dataType = columnTypeMap.get(columnName) || "text"; const convertedValue = this.convertValueForPostgreSQL(value, dataType); logger.info( @@ -2355,6 +2387,12 @@ export class TableManagementService { await query(insertQuery, values); logger.info(`테이블 데이터 추가 완료: ${tableName}`); + + // 무시된 컬럼과 저장된 컬럼 정보 반환 + return { + skippedColumns, + savedColumns: existingColumns, + }; } catch (error) { logger.error(`테이블 데이터 추가 오류: ${tableName}`, error); throw error; diff --git a/frontend/lib/utils/buttonActions.ts b/frontend/lib/utils/buttonActions.ts index e9327082..9b847ef3 100644 --- a/frontend/lib/utils/buttonActions.ts +++ b/frontend/lib/utils/buttonActions.ts @@ -1063,6 +1063,13 @@ export class ButtonActionExecutor { dataWithMeta, ); console.log("✅ [handleSave] RepeaterFieldGroup INSERT 완료:", insertResult.data); + // 무시된 컬럼이 있으면 경고 출력 + if (insertResult.data?.data?.skippedColumns?.length > 0) { + console.warn( + `⚠️ [${repeaterTargetTable}] 테이블에 존재하지 않는 컬럼이 무시됨:`, + insertResult.data.data.skippedColumns, + ); + } } else if (item.id) { // UPDATE (기존 항목) const originalData = { id: item.id }; From c1425be57fa888ee3fe490b15c95febaefde6f6a Mon Sep 17 00:00:00 2001 From: kjs Date: Tue, 6 Jan 2026 15:33:44 +0900 Subject: [PATCH 7/7] =?UTF-8?q?=EC=B0=BD=EA=B3=A0=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EA=B0=99=EC=9D=B4=20=EC=98=AC=EB=9D=BC=EA=B0=80=EA=B2=8C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../rack-structure/RackStructureComponent.tsx | 14 +++++++++++++- .../registry/components/rack-structure/types.ts | 2 +- frontend/lib/utils/buttonActions.ts | 12 ++++++------ 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/frontend/lib/registry/components/rack-structure/RackStructureComponent.tsx b/frontend/lib/registry/components/rack-structure/RackStructureComponent.tsx index d80fd2c7..77eadca0 100644 --- a/frontend/lib/registry/components/rack-structure/RackStructureComponent.tsx +++ b/frontend/lib/registry/components/rack-structure/RackStructureComponent.tsx @@ -605,7 +605,7 @@ export const RackStructureComponent: React.FC = ({ location_type: context?.locationType || "선반", status: context?.status || "사용", // 추가 필드 (테이블 컬럼명과 동일) - warehouse_id: context?.warehouseCode, + warehouse_code: context?.warehouseCode, warehouse_name: context?.warehouseName, floor: context?.floor, zone: context?.zone, @@ -623,6 +623,18 @@ export const RackStructureComponent: React.FC = ({ setPreviewData(locations); setIsPreviewGenerated(true); + + console.log("🏗️ [RackStructure] 생성된 위치 데이터:", { + locationsCount: locations.length, + firstLocation: locations[0], + context: { + warehouseCode: context?.warehouseCode, + warehouseName: context?.warehouseName, + floor: context?.floor, + zone: context?.zone, + }, + }); + onChange?.(locations); }, [conditions, context, generateLocationCode, onChange, missingFields, hasRowOverlap, duplicateErrors, existingLocations, rowOverlapErrors]); diff --git a/frontend/lib/registry/components/rack-structure/types.ts b/frontend/lib/registry/components/rack-structure/types.ts index 5ab7bd7e..8670d4a0 100644 --- a/frontend/lib/registry/components/rack-structure/types.ts +++ b/frontend/lib/registry/components/rack-structure/types.ts @@ -27,7 +27,7 @@ export interface GeneratedLocation { location_type?: string; // 위치 유형 status?: string; // 사용 여부 // 추가 필드 (상위 폼에서 매핑된 값) - warehouse_id?: string; // 창고 ID/코드 + warehouse_code?: string; // 창고 코드 (DB 컬럼명과 동일) warehouse_name?: string; // 창고명 floor?: string; // 층 zone?: string; // 구역 diff --git a/frontend/lib/utils/buttonActions.ts b/frontend/lib/utils/buttonActions.ts index 53373d41..099daa91 100644 --- a/frontend/lib/utils/buttonActions.ts +++ b/frontend/lib/utils/buttonActions.ts @@ -1374,17 +1374,17 @@ export class ButtonActionExecutor { // 저장 전 중복 체크 const firstLocation = locations[0]; - const warehouseId = firstLocation.warehouse_id || firstLocation.warehouseCode; + const warehouseCode = firstLocation.warehouse_code || firstLocation.warehouse_id || firstLocation.warehouseCode; const floor = firstLocation.floor; const zone = firstLocation.zone; - if (warehouseId && floor && zone) { - console.log("🔍 [handleRackStructureBatchSave] 기존 데이터 중복 체크:", { warehouseId, floor, zone }); + if (warehouseCode && floor && zone) { + console.log("🔍 [handleRackStructureBatchSave] 기존 데이터 중복 체크:", { warehouseCode, floor, zone }); try { const existingResponse = await DynamicFormApi.getTableData(tableName, { filters: { - warehouse_id: warehouseId, + warehouse_code: warehouseCode, floor: floor, zone: zone, }, @@ -1434,8 +1434,8 @@ export class ButtonActionExecutor { location_name: loc.location_name || loc.locationName, row_num: loc.row_num || String(loc.rowNum), level_num: loc.level_num || String(loc.levelNum), - // 창고 정보 (렉 구조 컴포넌트에서 전달) - warehouse_id: loc.warehouse_id || loc.warehouseCode, + // 창고 정보 (렉 구조 컴포넌트에서 전달) - DB 컬럼명은 warehouse_code + warehouse_code: loc.warehouse_code || loc.warehouse_id || loc.warehouseCode, warehouse_name: loc.warehouse_name || loc.warehouseName, // 위치 정보 (렉 구조 컴포넌트에서 전달) floor: loc.floor,