From 35f83c1937e34fb7028383fdc572d628cf4bd014 Mon Sep 17 00:00:00 2001 From: kjs Date: Fri, 9 Jan 2026 18:22:50 +0900 Subject: [PATCH 1/4] =?UTF-8?q?=EB=A1=9C=EA=B7=B8=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/common/ExcelUploadModal.tsx | 20 ++----------------- frontend/lib/utils/buttonActions.ts | 14 +------------ 2 files changed, 3 insertions(+), 31 deletions(-) diff --git a/frontend/components/common/ExcelUploadModal.tsx b/frontend/components/common/ExcelUploadModal.tsx index 6eda1594..64fe38b8 100644 --- a/frontend/components/common/ExcelUploadModal.tsx +++ b/frontend/components/common/ExcelUploadModal.tsx @@ -705,43 +705,27 @@ export const ExcelUploadModal: React.FC = ({ } } else { // 기존 단일 테이블 업로드 로직 - console.log("📊 단일 테이블 업로드 시작:", { - tableName, - uploadMode, - numberingRuleId, - numberingTargetColumn, - dataCount: filteredData.length, - }); - let successCount = 0; let failCount = 0; - // 🆕 단일 테이블 채번 설정 확인 + // 단일 테이블 채번 설정 확인 const hasNumbering = numberingRuleId && numberingTargetColumn; - console.log("📊 채번 설정:", { hasNumbering, numberingRuleId, numberingTargetColumn }); for (const row of filteredData) { try { let dataToSave = { ...row }; - // 🆕 채번 적용: 각 행마다 채번 API 호출 + // 채번 적용: 각 행마다 채번 API 호출 if (hasNumbering && uploadMode === "insert") { try { const { apiClient } = await import("@/lib/api/client"); - console.log(`📊 채번 API 호출: /numbering-rules/${numberingRuleId}/allocate`); const numberingResponse = await apiClient.post(`/numbering-rules/${numberingRuleId}/allocate`); - console.log(`📊 채번 API 응답:`, numberingResponse.data); - // 응답 구조: { success: true, data: { generatedCode: "..." } } const generatedCode = numberingResponse.data?.data?.generatedCode || numberingResponse.data?.data?.code; if (numberingResponse.data?.success && generatedCode) { dataToSave[numberingTargetColumn] = generatedCode; - console.log(`✅ 채번 적용: ${numberingTargetColumn} = ${generatedCode}`); - } else { - console.warn(`⚠️ 채번 실패: 응답에 코드 없음`, numberingResponse.data); } } catch (numError) { console.error("채번 오류:", numError); - // 채번 실패 시에도 계속 진행 (채번 컬럼만 비워둠) } } diff --git a/frontend/lib/utils/buttonActions.ts b/frontend/lib/utils/buttonActions.ts index 05777580..182e0cdc 100644 --- a/frontend/lib/utils/buttonActions.ts +++ b/frontend/lib/utils/buttonActions.ts @@ -4835,19 +4835,7 @@ export class ButtonActionExecutor { */ private static async handleExcelUpload(config: ButtonActionConfig, context: ButtonActionContext): Promise { try { - console.log("📤 엑셀 업로드 모달 열기:", { - config, - context, - userId: context.userId, - tableName: context.tableName, - screenId: context.screenId, - // 채번 설정 디버깅 - numberingRuleId: config.excelNumberingRuleId, - numberingTargetColumn: config.excelNumberingTargetColumn, - afterUploadFlows: config.excelAfterUploadFlows, - }); - - // 🆕 마스터-디테일 구조 확인 (화면에 분할 패널이 있으면 자동 감지) + // 마스터-디테일 구조 확인 (화면에 분할 패널이 있으면 자동 감지) let isMasterDetail = false; let masterDetailRelation: any = null; let masterDetailExcelConfig: any = undefined; -- 2.43.0 From 9e7253a29349ac59b18770fb6925ab648434058d Mon Sep 17 00:00:00 2001 From: kjs Date: Mon, 12 Jan 2026 10:32:41 +0900 Subject: [PATCH 2/4] =?UTF-8?q?=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC=20?= =?UTF-8?q?=EB=9D=BC=EB=B2=A8=20=EB=B3=B4=EC=9D=B4=EC=A7=80=20=EC=95=8A?= =?UTF-8?q?=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/services/tableCategoryValueService.ts | 117 +++++++++--------- .../screen/InteractiveDataTable.tsx | 21 +++- frontend/contexts/ScreenContext.tsx | 5 + 3 files changed, 79 insertions(+), 64 deletions(-) diff --git a/backend-node/src/services/tableCategoryValueService.ts b/backend-node/src/services/tableCategoryValueService.ts index edeb55b2..9cbbc521 100644 --- a/backend-node/src/services/tableCategoryValueService.ts +++ b/backend-node/src/services/tableCategoryValueService.ts @@ -187,71 +187,68 @@ class TableCategoryValueService { logger.info("형제 메뉴 OBJID 목록", { menuObjid, siblingObjids }); } - // 2. 카테고리 값 조회 (형제 메뉴 포함) + // 2. 카테고리 값 조회 (메뉴 스코프 또는 형제 메뉴 포함) let query: string; let params: any[]; + const baseSelect = ` + SELECT + value_id AS "valueId", + table_name AS "tableName", + column_name AS "columnName", + value_code AS "valueCode", + value_label AS "valueLabel", + value_order AS "valueOrder", + parent_value_id AS "parentValueId", + depth, + description, + color, + icon, + is_active AS "isActive", + is_default AS "isDefault", + company_code AS "companyCode", + menu_objid AS "menuObjid", + created_at AS "createdAt", + updated_at AS "updatedAt", + created_by AS "createdBy", + updated_by AS "updatedBy" + FROM table_column_category_values + WHERE table_name = $1 + AND column_name = $2 + `; + if (companyCode === "*") { - // 최고 관리자: 모든 카테고리 값 조회 - // 메뉴 스코프 제거: 같은 테이블.컬럼 조합은 모든 메뉴에서 공유 - query = ` - SELECT - value_id AS "valueId", - table_name AS "tableName", - column_name AS "columnName", - value_code AS "valueCode", - value_label AS "valueLabel", - value_order AS "valueOrder", - parent_value_id AS "parentValueId", - depth, - description, - color, - icon, - is_active AS "isActive", - is_default AS "isDefault", - company_code AS "companyCode", - menu_objid AS "menuObjid", - created_at AS "createdAt", - updated_at AS "updatedAt", - created_by AS "createdBy", - updated_by AS "updatedBy" - FROM table_column_category_values - WHERE table_name = $1 - AND column_name = $2 - `; - params = [tableName, columnName]; - logger.info("최고 관리자 카테고리 값 조회"); + // 최고 관리자: menuObjid가 있으면 해당 메뉴(및 형제 메뉴)의 값만 조회 + if (menuObjid && siblingObjids.length > 0) { + query = baseSelect + ` AND menu_objid = ANY($3::numeric[])`; + params = [tableName, columnName, siblingObjids]; + logger.info("최고 관리자 메뉴 스코프 카테고리 값 조회", { menuObjid, siblingObjids }); + } else if (menuObjid) { + query = baseSelect + ` AND menu_objid = $3`; + params = [tableName, columnName, menuObjid]; + logger.info("최고 관리자 단일 메뉴 카테고리 값 조회", { menuObjid }); + } else { + // menuObjid 없으면 모든 값 조회 (중복 가능) + query = baseSelect; + params = [tableName, columnName]; + logger.info("최고 관리자 전체 카테고리 값 조회 (menuObjid 없음)"); + } } else { - // 일반 회사: 자신의 카테고리 값만 조회 - // 메뉴 스코프 제거: 같은 테이블.컬럼 조합은 모든 메뉴에서 공유 - query = ` - SELECT - value_id AS "valueId", - table_name AS "tableName", - column_name AS "columnName", - value_code AS "valueCode", - value_label AS "valueLabel", - value_order AS "valueOrder", - parent_value_id AS "parentValueId", - depth, - description, - color, - icon, - is_active AS "isActive", - is_default AS "isDefault", - company_code AS "companyCode", - menu_objid AS "menuObjid", - created_at AS "createdAt", - updated_at AS "updatedAt", - created_by AS "createdBy", - updated_by AS "updatedBy" - FROM table_column_category_values - WHERE table_name = $1 - AND column_name = $2 - AND company_code = $3 - `; - params = [tableName, columnName, companyCode]; - logger.info("회사별 카테고리 값 조회", { companyCode }); + // 일반 회사: 자신의 회사 + menuObjid로 필터링 + if (menuObjid && siblingObjids.length > 0) { + query = baseSelect + ` AND company_code = $3 AND menu_objid = ANY($4::numeric[])`; + params = [tableName, columnName, companyCode, siblingObjids]; + logger.info("회사별 메뉴 스코프 카테고리 값 조회", { companyCode, menuObjid, siblingObjids }); + } else if (menuObjid) { + query = baseSelect + ` AND company_code = $3 AND menu_objid = $4`; + params = [tableName, columnName, companyCode, menuObjid]; + logger.info("회사별 단일 메뉴 카테고리 값 조회", { companyCode, menuObjid }); + } else { + // menuObjid 없으면 회사 전체 조회 (중복 가능하지만 회사별로 제한) + query = baseSelect + ` AND company_code = $3`; + params = [tableName, columnName, companyCode]; + logger.info("회사별 카테고리 값 조회 (menuObjid 없음)", { companyCode }); + } } if (!includeInactive) { diff --git a/frontend/components/screen/InteractiveDataTable.tsx b/frontend/components/screen/InteractiveDataTable.tsx index 5e4bda2e..7fcc61ed 100644 --- a/frontend/components/screen/InteractiveDataTable.tsx +++ b/frontend/components/screen/InteractiveDataTable.tsx @@ -1,6 +1,7 @@ "use client"; -import React, { useState, useEffect, useCallback, useRef } from "react"; +import React, { useState, useEffect, useCallback, useRef, useMemo } from "react"; +import { useSearchParams } from "next/navigation"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; @@ -188,6 +189,16 @@ export const InteractiveDataTable: React.FC = ({ const screenContext = useScreenContextOptional(); // 화면 컨텍스트 (좌측/우측 위치 확인용) const splitPanelPosition = screenContext?.splitPanelPosition; // 분할 패널 내 위치 + // URL에서 menuObjid 가져오기 (카테고리 값 조회 시 필요) + const searchParams = useSearchParams(); + const menuObjid = useMemo(() => { + // 1. ScreenContext에서 가져오기 + if (screenContext?.menuObjid) return screenContext.menuObjid; + // 2. URL 쿼리에서 가져오기 + const urlMenuObjid = searchParams.get("menuObjid"); + return urlMenuObjid ? parseInt(urlMenuObjid) : undefined; + }, [screenContext?.menuObjid, searchParams]); + const [data, setData] = useState[]>([]); const [loading, setLoading] = useState(false); const [searchValues, setSearchValues] = useState>({}); @@ -365,8 +376,10 @@ export const InteractiveDataTable: React.FC = ({ for (const col of categoryColumns) { try { + // menuObjid가 있으면 쿼리 파라미터로 전달 (메뉴별 카테고리 색상 적용) + const queryParams = menuObjid ? `?menuObjid=${menuObjid}` : ""; const response = await apiClient.get( - `/table-categories/${component.tableName}/${col.columnName}/values` + `/table-categories/${component.tableName}/${col.columnName}/values${queryParams}` ); if (response.data.success && response.data.data) { @@ -379,7 +392,7 @@ export const InteractiveDataTable: React.FC = ({ }; }); mappings[col.columnName] = mapping; - console.log(`✅ 카테고리 매핑 로드 성공 [${col.columnName}]:`, mapping); + console.log(`✅ 카테고리 매핑 로드 성공 [${col.columnName}]:`, mapping, { menuObjid }); } } catch (error) { console.error(`❌ 카테고리 값 로드 실패 [${col.columnName}]:`, error); @@ -394,7 +407,7 @@ export const InteractiveDataTable: React.FC = ({ }; loadCategoryMappings(); - }, [component.tableName, component.columns, getColumnWebType]); + }, [component.tableName, component.columns, getColumnWebType, menuObjid]); // 파일 상태 확인 함수 const checkFileStatus = useCallback( diff --git a/frontend/contexts/ScreenContext.tsx b/frontend/contexts/ScreenContext.tsx index 0bb6a32c..5e9bb2f1 100644 --- a/frontend/contexts/ScreenContext.tsx +++ b/frontend/contexts/ScreenContext.tsx @@ -13,6 +13,7 @@ import type { SplitPanelPosition } from "@/contexts/SplitPanelContext"; interface ScreenContextValue { screenId?: number; tableName?: string; + menuObjid?: number; // 메뉴 OBJID (카테고리 값 조회 시 필요) splitPanelPosition?: SplitPanelPosition; // 🆕 분할 패널 위치 (left/right) // 🆕 폼 데이터 (RepeaterFieldGroup 등 컴포넌트 데이터 저장) @@ -39,6 +40,7 @@ const ScreenContext = createContext(null); interface ScreenContextProviderProps { screenId?: number; tableName?: string; + menuObjid?: number; // 메뉴 OBJID splitPanelPosition?: SplitPanelPosition; // 🆕 분할 패널 위치 children: React.ReactNode; } @@ -49,6 +51,7 @@ interface ScreenContextProviderProps { export function ScreenContextProvider({ screenId, tableName, + menuObjid, splitPanelPosition, children, }: ScreenContextProviderProps) { @@ -112,6 +115,7 @@ export function ScreenContextProvider({ () => ({ screenId, tableName, + menuObjid, splitPanelPosition, formData, updateFormData, @@ -127,6 +131,7 @@ export function ScreenContextProvider({ [ screenId, tableName, + menuObjid, splitPanelPosition, formData, updateFormData, -- 2.43.0 From 9cc5bbbf05402dbcb547039e4ab78316b9658e43 Mon Sep 17 00:00:00 2001 From: kjs Date: Mon, 12 Jan 2026 13:53:57 +0900 Subject: [PATCH 3/4] =?UTF-8?q?=EC=97=91=EC=85=80=20=EC=97=85=EB=A1=9C?= =?UTF-8?q?=EB=93=9C=20=EB=AC=B8=EC=A0=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/services/masterDetailExcelService.ts | 26 ++++++++++++++++--- frontend/lib/utils/buttonActions.ts | 8 ++++++ 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/backend-node/src/services/masterDetailExcelService.ts b/backend-node/src/services/masterDetailExcelService.ts index 4b1a7218..0c1dfafe 100644 --- a/backend-node/src/services/masterDetailExcelService.ts +++ b/backend-node/src/services/masterDetailExcelService.ts @@ -47,8 +47,13 @@ export interface SplitPanelConfig { columns: Array<{ name: string; label: string; width?: number }>; relation?: { type: string; - foreignKey: string; - leftColumn: string; + foreignKey?: string; + leftColumn?: string; + // 복합키 지원 (새로운 방식) + keys?: Array<{ + leftColumn: string; + rightColumn: string; + }>; }; }; } @@ -210,8 +215,21 @@ class MasterDetailExcelService { } // 2. 분할 패널의 relation 정보가 있으면 우선 사용 - let masterKeyColumn = splitPanel.rightPanel.relation?.leftColumn; - let detailFkColumn = splitPanel.rightPanel.relation?.foreignKey; + // 🔥 keys 배열을 우선 사용 (새로운 복합키 지원 방식) + let masterKeyColumn: string | undefined; + let detailFkColumn: string | undefined; + + const relationKeys = splitPanel.rightPanel.relation?.keys; + if (relationKeys && relationKeys.length > 0) { + // keys 배열에서 첫 번째 키 사용 + masterKeyColumn = relationKeys[0].leftColumn; + detailFkColumn = relationKeys[0].rightColumn; + logger.info(`keys 배열에서 관계 정보 사용: ${masterKeyColumn} -> ${detailFkColumn}`); + } else { + // 하위 호환성: 기존 leftColumn/foreignKey 사용 + masterKeyColumn = splitPanel.rightPanel.relation?.leftColumn; + detailFkColumn = splitPanel.rightPanel.relation?.foreignKey; + } // 3. relation 정보가 없으면 column_labels에서 Entity 관계 조회 if (!masterKeyColumn || !detailFkColumn) { diff --git a/frontend/lib/utils/buttonActions.ts b/frontend/lib/utils/buttonActions.ts index 182e0cdc..f03774f1 100644 --- a/frontend/lib/utils/buttonActions.ts +++ b/frontend/lib/utils/buttonActions.ts @@ -4858,6 +4858,10 @@ export class ButtonActionExecutor { detailTable: relationResponse.data.detailTable, masterKeyColumn: relationResponse.data.masterKeyColumn, detailFkColumn: relationResponse.data.detailFkColumn, + // 채번 규칙 ID 추가 (excelNumberingRuleId를 numberingRuleId로 매핑) + numberingRuleId: config.masterDetailExcel.numberingRuleId || config.excelNumberingRuleId, + // 업로드 후 제어 설정 추가 + afterUploadFlows: config.masterDetailExcel.afterUploadFlows || config.excelAfterUploadFlows, }; } else { // 버튼 설정이 없으면 분할 패널 정보만 사용 @@ -4867,6 +4871,10 @@ export class ButtonActionExecutor { masterKeyColumn: relationResponse.data.masterKeyColumn, detailFkColumn: relationResponse.data.detailFkColumn, simpleMode: true, // 기본값으로 간단 모드 사용 + // 채번 규칙 ID 추가 (excelNumberingRuleId 사용) + numberingRuleId: config.excelNumberingRuleId, + // 업로드 후 제어 설정 추가 + afterUploadFlows: config.excelAfterUploadFlows, }; } -- 2.43.0 From 87189c792e62ae6a0efdb27dd93038b6a11ccd24 Mon Sep 17 00:00:00 2001 From: kjs Date: Mon, 12 Jan 2026 16:08:02 +0900 Subject: [PATCH 4/4] =?UTF-8?q?=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC?= =?UTF-8?q?=EA=B0=92=20=EC=9E=90=EB=8F=99=EA=B0=90=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../screen/InteractiveDataTable.tsx | 307 +++++++++++------- .../SplitPanelLayoutComponent.tsx | 57 +++- .../TableSectionRenderer.tsx | 51 ++- .../UniversalFormModalConfigPanel.tsx | 29 +- .../modals/TableColumnSettingsModal.tsx | 24 +- .../modals/TableSectionSettingsModal.tsx | 31 +- .../components/universal-form-modal/types.ts | 1 + 7 files changed, 365 insertions(+), 135 deletions(-) diff --git a/frontend/components/screen/InteractiveDataTable.tsx b/frontend/components/screen/InteractiveDataTable.tsx index 7fcc61ed..8c4c95ce 100644 --- a/frontend/components/screen/InteractiveDataTable.tsx +++ b/frontend/components/screen/InteractiveDataTable.tsx @@ -43,7 +43,7 @@ import { } from "lucide-react"; import { tableTypeApi } from "@/lib/api/screen"; import { commonCodeApi } from "@/lib/api/commonCode"; -import { getCurrentUser, UserInfo } from "@/lib/api/client"; +import { apiClient, getCurrentUser, UserInfo } from "@/lib/api/client"; import { useAuth } from "@/hooks/useAuth"; import { DataTableComponent, DataTableColumn, DataTableFilter } from "@/types/screen-legacy-backup"; import { cn } from "@/lib/utils"; @@ -101,11 +101,7 @@ const CascadingDropdownInForm: React.FC = ({ const isDisabled = !parentValue || loading; return ( - onChange?.(newValue)} disabled={isDisabled}> {loading ? (
@@ -188,7 +184,7 @@ export const InteractiveDataTable: React.FC = ({ const splitPanelContext = useSplitPanelContext(); // 분할 패널 컨텍스트 const screenContext = useScreenContextOptional(); // 화면 컨텍스트 (좌측/우측 위치 확인용) const splitPanelPosition = screenContext?.splitPanelPosition; // 분할 패널 내 위치 - + // URL에서 menuObjid 가져오기 (카테고리 값 조회 시 필요) const searchParams = useSearchParams(); const menuObjid = useMemo(() => { @@ -198,7 +194,7 @@ export const InteractiveDataTable: React.FC = ({ const urlMenuObjid = searchParams.get("menuObjid"); return urlMenuObjid ? parseInt(urlMenuObjid) : undefined; }, [screenContext?.menuObjid, searchParams]); - + const [data, setData] = useState[]>([]); const [loading, setLoading] = useState(false); const [searchValues, setSearchValues] = useState>({}); @@ -210,7 +206,7 @@ export const InteractiveDataTable: React.FC = ({ const hasInitializedWidthsRef = useRef(false); const columnRefs = useRef>({}); const isResizingRef = useRef(false); - + // TableOptions 상태 const [filters, setFilters] = useState([]); const [grouping, setGrouping] = useState([]); @@ -247,14 +243,19 @@ export const InteractiveDataTable: React.FC = ({ const [columnLabels, setColumnLabels] = useState>({}); // 컬럼명 -> 라벨 매핑 // 카테고리 값 매핑 캐시 (컬럼명 -> {코드 -> {라벨, 색상}}) - const [categoryMappings, setCategoryMappings] = useState>>({}); + const [categoryMappings, setCategoryMappings] = useState< + Record> + >({}); + + // 카테고리 코드 라벨 캐시 (CATEGORY_* 코드 -> 라벨) + const [categoryCodeLabels, setCategoryCodeLabels] = useState>({}); // 테이블 등록 (Context에 등록) const tableId = `datatable-${component.id}`; - + useEffect(() => { if (!component.tableName || !component.columns) return; - + registerTable({ tableId, label: component.title || "데이터 테이블", @@ -331,7 +332,7 @@ export const InteractiveDataTable: React.FC = ({ useEffect(() => { const handleRelatedButtonSelect = (event: CustomEvent) => { const { targetTable, filterColumn, filterValue } = event.detail || {}; - + // 이 테이블이 대상 테이블인지 확인 if (targetTable === component.tableName) { console.log("📌 [InteractiveDataTable] RelatedDataButtons 필터 적용:", { @@ -379,7 +380,7 @@ export const InteractiveDataTable: React.FC = ({ // menuObjid가 있으면 쿼리 파라미터로 전달 (메뉴별 카테고리 색상 적용) const queryParams = menuObjid ? `?menuObjid=${menuObjid}` : ""; const response = await apiClient.get( - `/table-categories/${component.tableName}/${col.columnName}/values${queryParams}` + `/table-categories/${component.tableName}/${col.columnName}/values${queryParams}`, ); if (response.data.success && response.data.data) { @@ -596,13 +597,13 @@ export const InteractiveDataTable: React.FC = ({ // 없으면 테이블 타입 관리에서 설정된 값 찾기 const tableColumn = tableColumns.find((col) => col.columnName === columnName); - + // input_type 우선 사용 (category 등) const inputType = (tableColumn as any)?.input_type || (tableColumn as any)?.inputType; if (inputType) { return inputType; } - + // 없으면 webType 사용 return tableColumn?.webType || "text"; }, @@ -709,19 +710,19 @@ export const InteractiveDataTable: React.FC = ({ let linkedFilterValues: Record = {}; let hasLinkedFiltersConfigured = false; // 연결 필터가 설정되어 있는지 여부 let hasSelectedLeftData = false; // 좌측에서 데이터가 선택되었는지 여부 - + if (splitPanelContext) { // 연결 필터 설정 여부 확인 (현재 테이블에 해당하는 필터가 있는지) const linkedFiltersConfig = splitPanelContext.linkedFilters || []; hasLinkedFiltersConfigured = linkedFiltersConfig.some( - (filter) => filter.targetColumn?.startsWith(component.tableName + ".") || - filter.targetColumn === component.tableName + (filter) => + filter.targetColumn?.startsWith(component.tableName + ".") || filter.targetColumn === component.tableName, ); - + // 좌측 데이터 선택 여부 확인 - hasSelectedLeftData = splitPanelContext.selectedLeftData && - Object.keys(splitPanelContext.selectedLeftData).length > 0; - + hasSelectedLeftData = + splitPanelContext.selectedLeftData && Object.keys(splitPanelContext.selectedLeftData).length > 0; + linkedFilterValues = splitPanelContext.getLinkedFilterValues(); // 현재 테이블에 해당하는 필터만 추출 (테이블명.컬럼명 형식에서) const tableSpecificFilters: Record = {}; @@ -740,7 +741,7 @@ export const InteractiveDataTable: React.FC = ({ } linkedFilterValues = tableSpecificFilters; } - + // 🆕 연결 필터가 설정되어 있지만 좌측에서 데이터가 선택되지 않은 경우 // → 빈 데이터 표시 (모든 데이터를 보여주지 않음) if (hasLinkedFiltersConfigured && !hasSelectedLeftData) { @@ -752,9 +753,9 @@ export const InteractiveDataTable: React.FC = ({ setLoading(false); return; } - + // 🆕 RelatedDataButtons 필터 적용 - let relatedButtonFilterValues: Record = {}; + const relatedButtonFilterValues: Record = {}; if (relatedButtonFilter) { relatedButtonFilterValues[relatedButtonFilter.filterColumn] = relatedButtonFilter.filterValue; } @@ -765,16 +766,16 @@ export const InteractiveDataTable: React.FC = ({ ...linkedFilterValues, ...relatedButtonFilterValues, // 🆕 RelatedDataButtons 필터 추가 }; - - console.log("🔍 데이터 조회 시작:", { - tableName: component.tableName, - page, + + console.log("🔍 데이터 조회 시작:", { + tableName: component.tableName, + page, pageSize, linkedFilterValues, relatedButtonFilterValues, mergedSearchParams, }); - + const result = await tableTypeApi.getTableData(component.tableName, { page, size: pageSize, @@ -782,11 +783,11 @@ export const InteractiveDataTable: React.FC = ({ autoFilter: component.autoFilter, // 🆕 자동 필터 설정 전달 }); - console.log("✅ 데이터 조회 완료:", { + console.log("✅ 데이터 조회 완료:", { tableName: component.tableName, - dataLength: result.data.length, + dataLength: result.data.length, total: result.total, - page: result.page + page: result.page, }); setData(result.data); @@ -794,6 +795,45 @@ export const InteractiveDataTable: React.FC = ({ setTotalPages(result.totalPages); setCurrentPage(result.page); + // 카테고리 코드 패턴(CATEGORY_*) 검출 및 라벨 조회 + const detectAndLoadCategoryLabels = async () => { + const categoryCodes = new Set(); + result.data.forEach((row: Record) => { + Object.values(row).forEach((value) => { + if (typeof value === "string" && value.startsWith("CATEGORY_")) { + categoryCodes.add(value); + } + }); + }); + + console.log("🏷️ [InteractiveDataTable] 감지된 카테고리 코드:", Array.from(categoryCodes)); + + // 새로운 카테고리 코드만 필터링 (이미 캐시된 것 제외) + const newCodes = Array.from(categoryCodes); + + if (newCodes.length > 0) { + try { + console.log("🏷️ [InteractiveDataTable] 카테고리 라벨 API 호출:", newCodes); + const response = await apiClient.post("/table-categories/labels-by-codes", { valueCodes: newCodes }); + console.log("🏷️ [InteractiveDataTable] 카테고리 라벨 API 응답:", response.data); + if (response.data.success && response.data.data) { + setCategoryCodeLabels((prev) => { + const newLabels = { + ...prev, + ...response.data.data, + }; + console.log("🏷️ [InteractiveDataTable] 카테고리 라벨 캐시 업데이트:", newLabels); + return newLabels; + }); + } + } catch (error) { + console.error("카테고리 라벨 조회 실패:", error); + } + } + }; + + detectAndLoadCategoryLabels(); + // 각 행의 파일 상태 확인 (전체 행 + 가상 파일 컬럼별) const fileStatusPromises = result.data.map(async (rowData: Record) => { const primaryKeyField = Object.keys(rowData)[0]; @@ -929,18 +969,18 @@ export const InteractiveDataTable: React.FC = ({ try { const columns = await tableTypeApi.getColumns(component.tableName); setTableColumns(columns); - + // 🆕 전체 컬럼 목록 설정 - const columnNames = columns.map(col => col.columnName); + const columnNames = columns.map((col) => col.columnName); setAllAvailableColumns(columnNames); - + // 🆕 컬럼명 -> 라벨 매핑 생성 const labels: Record = {}; - columns.forEach(col => { + columns.forEach((col) => { labels[col.columnName] = col.displayName || col.columnName; }); setColumnLabels(labels); - + // 🆕 localStorage에서 필터 설정 복원 if (user?.userId && component.componentId) { const storageKey = `table-search-filter-${user.userId}-${component.componentId}`; @@ -996,28 +1036,31 @@ export const InteractiveDataTable: React.FC = ({ ); // 행 선택 핸들러 - const handleRowSelect = useCallback((rowIndex: number, isSelected: boolean) => { - setSelectedRows((prev) => { - const newSet = new Set(prev); - if (isSelected) { - newSet.add(rowIndex); - } else { - newSet.delete(rowIndex); + const handleRowSelect = useCallback( + (rowIndex: number, isSelected: boolean) => { + setSelectedRows((prev) => { + const newSet = new Set(prev); + if (isSelected) { + newSet.add(rowIndex); + } else { + newSet.delete(rowIndex); + } + return newSet; + }); + + // 분할 패널 좌측에서 선택 시 컨텍스트에 데이터 저장 (연결 필터용) + if (splitPanelContext && splitPanelPosition === "left" && !splitPanelContext.disableAutoDataTransfer) { + if (isSelected && data[rowIndex]) { + splitPanelContext.setSelectedLeftData(data[rowIndex]); + console.log("🔗 [InteractiveDataTable] 좌측 선택 데이터 저장:", data[rowIndex]); + } else if (!isSelected) { + splitPanelContext.setSelectedLeftData(null); + console.log("🔗 [InteractiveDataTable] 좌측 선택 데이터 초기화"); + } } - return newSet; - }); - - // 분할 패널 좌측에서 선택 시 컨텍스트에 데이터 저장 (연결 필터용) - if (splitPanelContext && splitPanelPosition === "left" && !splitPanelContext.disableAutoDataTransfer) { - if (isSelected && data[rowIndex]) { - splitPanelContext.setSelectedLeftData(data[rowIndex]); - console.log("🔗 [InteractiveDataTable] 좌측 선택 데이터 저장:", data[rowIndex]); - } else if (!isSelected) { - splitPanelContext.setSelectedLeftData(null); - console.log("🔗 [InteractiveDataTable] 좌측 선택 데이터 초기화"); - } - } - }, [data, splitPanelContext, splitPanelPosition]); + }, + [data, splitPanelContext, splitPanelPosition], + ); // 전체 선택/해제 핸들러 const handleSelectAll = useCallback( @@ -1599,7 +1642,7 @@ export const InteractiveDataTable: React.FC = ({
); } - + // 상세 설정에서 옵션 목록 가져오기 const options = detailSettings?.options || []; if (options.length > 0) { @@ -1726,7 +1769,9 @@ export const InteractiveDataTable: React.FC = ({ case "category": { // 카테고리 셀렉트 (동적 import) - const { CategorySelectComponent } = require("@/lib/registry/components/category-select/CategorySelectComponent"); + const { + CategorySelectComponent, + } = require("@/lib/registry/components/category-select/CategorySelectComponent"); return (
= ({
); } - + // 상세 설정에서 옵션 목록 가져오기 const optionsAdd = detailSettings?.options || []; if (optionsAdd.length > 0) { @@ -2026,7 +2071,9 @@ export const InteractiveDataTable: React.FC = ({ case "category": { // 카테고리 셀렉트 (동적 import) - const { CategorySelectComponent } = require("@/lib/registry/components/category-select/CategorySelectComponent"); + const { + CategorySelectComponent, + } = require("@/lib/registry/components/category-select/CategorySelectComponent"); return (
= ({ const actualWebType = getColumnWebType(column.columnName); // 파일 타입 컬럼 처리 (가상 파일 컬럼 포함) - const isFileColumn = - actualWebType === "file" || column.columnName === "file_path" || column.isVirtualFileColumn; + const isFileColumn = actualWebType === "file" || column.columnName === "file_path" || column.isVirtualFileColumn; // 파일 타입 컬럼은 파일 아이콘으로 표시 (컬럼별 파일 관리) if (isFileColumn && rowData) { @@ -2210,25 +2256,25 @@ export const InteractiveDataTable: React.FC = ({ case "category": { // 카테고리 타입: 배지로 표시 (배지 없음 옵션 지원) if (!value) return ""; - + const mapping = categoryMappings[column.columnName]; const categoryData = mapping?.[String(value)]; - + // 매핑 데이터가 있으면 라벨과 색상 사용, 없으면 코드값만 텍스트로 표시 const displayLabel = categoryData?.label || String(value); const displayColor = categoryData?.color; - + // 배지 없음 옵션: color가 없거나, "none"이거나, 매핑 데이터가 없으면 텍스트만 표시 if (!displayColor || displayColor === "none" || !categoryData) { return {displayLabel}; } - + return ( - {displayLabel} @@ -2268,8 +2314,41 @@ export const InteractiveDataTable: React.FC = ({ } break; - default: - return String(value); + default: { + // 카테고리 코드 패턴 감지 (CATEGORY_로 시작하는 값) + const strValue = String(value); + if (strValue.startsWith("CATEGORY_")) { + // 1. categoryMappings에서 해당 코드 검색 (색상 정보 포함) + for (const columnName of Object.keys(categoryMappings)) { + const mapping = categoryMappings[columnName]; + const categoryData = mapping?.[strValue]; + if (categoryData) { + // 색상이 있으면 배지로, 없으면 텍스트로 표시 + if (categoryData.color && categoryData.color !== "none") { + return ( + + {categoryData.label} + + ); + } + return {categoryData.label}; + } + } + + // 2. categoryCodeLabels에서 검색 (API로 조회한 라벨) + const cachedLabel = categoryCodeLabels[strValue]; + if (cachedLabel) { + return {cachedLabel}; + } + } + return strValue; + } } return String(value); @@ -2405,15 +2484,12 @@ export const InteractiveDataTable: React.FC = ({ {visibleColumns.length > 0 ? ( <>
- - +
+ {/* 체크박스 컬럼 (삭제 기능이 활성화된 경우) */} {component.enableDelete && ( - + 0} onCheckedChange={handleSelectAll} @@ -2422,74 +2498,74 @@ export const InteractiveDataTable: React.FC = ({ )} {visibleColumns.map((column: DataTableColumn, columnIndex) => { const columnWidth = columnWidths[column.id]; - + return ( (columnRefs.current[column.id] = el)} - className="relative px-4 font-bold text-foreground/90 select-none text-center hover:bg-muted/70 transition-colors" - style={{ + className="text-foreground/90 hover:bg-muted/70 relative px-4 text-center font-bold transition-colors select-none" + style={{ width: columnWidth ? `${columnWidth}px` : undefined, - userSelect: 'none' + userSelect: "none", }} > {column.label} {/* 리사이즈 핸들 */} {columnIndex < visibleColumns.length - 1 && (
e.stopPropagation()} onMouseDown={(e) => { e.preventDefault(); e.stopPropagation(); - + const thElement = columnRefs.current[column.id]; if (!thElement) return; - + isResizingRef.current = true; - + const startX = e.clientX; const startWidth = columnWidth || thElement.offsetWidth; - + // 드래그 중 텍스트 선택 방지 - document.body.style.userSelect = 'none'; - document.body.style.cursor = 'col-resize'; - + document.body.style.userSelect = "none"; + document.body.style.cursor = "col-resize"; + const handleMouseMove = (moveEvent: MouseEvent) => { moveEvent.preventDefault(); - + const diff = moveEvent.clientX - startX; const newWidth = Math.max(80, startWidth + diff); - + // 직접 DOM 스타일 변경 (리렌더링 없음) if (thElement) { thElement.style.width = `${newWidth}px`; } }; - + const handleMouseUp = () => { // 최종 너비를 state에 저장 if (thElement) { const finalWidth = Math.max(80, thElement.offsetWidth); - setColumnWidths(prev => ({ ...prev, [column.id]: finalWidth })); + setColumnWidths((prev) => ({ ...prev, [column.id]: finalWidth })); } - + // 텍스트 선택 복원 - document.body.style.userSelect = ''; - document.body.style.cursor = ''; - + document.body.style.userSelect = ""; + document.body.style.cursor = ""; + // 약간의 지연 후 리사이즈 플래그 해제 setTimeout(() => { isResizingRef.current = false; }, 100); - - document.removeEventListener('mousemove', handleMouseMove); - document.removeEventListener('mouseup', handleMouseUp); + + document.removeEventListener("mousemove", handleMouseMove); + document.removeEventListener("mouseup", handleMouseUp); }; - - document.addEventListener('mousemove', handleMouseMove); - document.addEventListener('mouseup', handleMouseUp); + + document.addEventListener("mousemove", handleMouseMove); + document.addEventListener("mouseup", handleMouseUp); }} /> )} @@ -2517,10 +2593,7 @@ export const InteractiveDataTable: React.FC = ({ {/* 체크박스 셀 (삭제 기능이 활성화된 경우) */} {component.enableDelete && ( - + handleRowSelect(rowIndex, checked as boolean)} @@ -2530,10 +2603,10 @@ export const InteractiveDataTable: React.FC = ({ {visibleColumns.map((column: DataTableColumn) => { const isNumeric = column.widgetType === "number" || column.widgetType === "decimal"; return ( - {formatCellValue(row[column.columnName], column, row)} diff --git a/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx b/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx index 9da76559..4a21596e 100644 --- a/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx +++ b/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx @@ -185,6 +185,10 @@ export const SplitPanelLayoutComponent: React.FC const [rightCategoryMappings, setRightCategoryMappings] = useState< Record> >({}); // 우측 카테고리 매핑 + + // 카테고리 코드 라벨 캐시 (CATEGORY_* 코드 -> 라벨) + const [categoryCodeLabels, setCategoryCodeLabels] = useState>({}); + const { toast } = useToast(); // 추가 모달 상태 @@ -713,6 +717,14 @@ export const SplitPanelLayoutComponent: React.FC ); } + // 🆕 카테고리 코드 패턴 감지 (CATEGORY_로 시작하는 값) + if (typeof value === "string" && value.startsWith("CATEGORY_")) { + const cachedLabel = categoryCodeLabels[value]; + if (cachedLabel) { + return {cachedLabel}; + } + } + // 🆕 자동 날짜 감지 (ISO 8601 형식 또는 Date 객체) if (typeof value === "string" && value.match(/^\d{4}-\d{2}-\d{2}(T|\s)/)) { return formatDateValue(value, "YYYY-MM-DD"); @@ -734,7 +746,7 @@ export const SplitPanelLayoutComponent: React.FC // 일반 값 return String(value); }, - [formatDateValue, formatNumberValue], + [formatDateValue, formatNumberValue, categoryCodeLabels], ); // 좌측 데이터 로드 @@ -1079,6 +1091,49 @@ export const SplitPanelLayoutComponent: React.FC ], ); + // 🆕 카테고리 코드 라벨 로드 (rightData 변경 시) + useEffect(() => { + const loadCategoryCodeLabels = async () => { + if (!rightData) return; + + const categoryCodes = new Set(); + + // rightData가 배열인 경우 (조인 모드) + const dataArray = Array.isArray(rightData) ? rightData : [rightData]; + + dataArray.forEach((row: Record) => { + if (row) { + Object.values(row).forEach((value) => { + if (typeof value === "string" && value.startsWith("CATEGORY_")) { + categoryCodes.add(value); + } + }); + } + }); + + // 새로운 카테고리 코드만 필터링 (이미 캐시된 것 제외) + const newCodes = Array.from(categoryCodes).filter((code) => !categoryCodeLabels[code]); + + if (newCodes.length > 0) { + try { + console.log("🏷️ [SplitPanel] 카테고리 코드 라벨 조회:", newCodes); + const response = await apiClient.post("/table-categories/labels-by-codes", { valueCodes: newCodes }); + if (response.data.success && response.data.data) { + console.log("🏷️ [SplitPanel] 카테고리 라벨 조회 결과:", response.data.data); + setCategoryCodeLabels((prev) => ({ + ...prev, + ...response.data.data, + })); + } + } catch (error) { + console.error("카테고리 라벨 조회 실패:", error); + } + } + }; + + loadCategoryCodeLabels(); + }, [rightData]); + // 🆕 추가 탭 데이터 로딩 함수 const loadTabData = useCallback( async (tabIndex: number, leftItem: any) => { diff --git a/frontend/lib/registry/components/universal-form-modal/TableSectionRenderer.tsx b/frontend/lib/registry/components/universal-form-modal/TableSectionRenderer.tsx index ac43e1ed..120022a5 100644 --- a/frontend/lib/registry/components/universal-form-modal/TableSectionRenderer.tsx +++ b/frontend/lib/registry/components/universal-form-modal/TableSectionRenderer.tsx @@ -398,6 +398,9 @@ export function TableSectionRenderer({ // 소스 테이블의 컬럼 라벨 (API에서 동적 로드) const [sourceColumnLabels, setSourceColumnLabels] = useState>({}); + // 카테고리 타입 컬럼의 옵션 (column.type === "category") + const [categoryOptionsMap, setCategoryOptionsMap] = useState>({}); + // 외부 데이터(groupedData) 처리: 데이터 전달 모달열기 액션으로 전달받은 데이터를 초기 테이블 데이터로 설정 useEffect(() => { // 외부 데이터 소스가 활성화되지 않았거나, groupedData가 없으면 스킵 @@ -511,6 +514,46 @@ export function TableSectionRenderer({ loadColumnLabels(); }, [tableConfig.source.tableName, tableConfig.source.columnLabels]); + // 카테고리 타입 컬럼의 옵션 로드 + useEffect(() => { + const loadCategoryOptions = async () => { + const sourceTableName = tableConfig.source.tableName; + if (!sourceTableName) return; + if (!tableConfig.columns) return; + + // 카테고리 타입인 컬럼만 필터링 + const categoryColumns = tableConfig.columns.filter((col) => col.type === "category"); + if (categoryColumns.length === 0) return; + + const newOptionsMap: Record = {}; + + for (const col of categoryColumns) { + // 소스 필드 또는 필드명으로 카테고리 값 조회 + const actualColumnName = col.sourceField || col.field; + if (!actualColumnName) continue; + + try { + const { getCategoryValues } = await import("@/lib/api/tableCategoryValue"); + const result = await getCategoryValues(sourceTableName, actualColumnName, false); + + if (result && result.success && Array.isArray(result.data)) { + const options = result.data.map((item: any) => ({ + value: item.valueCode || item.value_code || item.value || "", + label: item.valueLabel || item.displayLabel || item.display_label || item.label || item.valueCode || item.value_code || item.value || "", + })); + newOptionsMap[col.field] = options; + } + } catch (error) { + console.error(`카테고리 옵션 로드 실패 (${col.field}):`, error); + } + } + + setCategoryOptionsMap((prev) => ({ ...prev, ...newOptionsMap })); + }; + + loadCategoryOptions(); + }, [tableConfig.source.tableName, tableConfig.columns]); + // 조건부 테이블: 동적 옵션 로드 (optionSource 설정이 있는 경우) useEffect(() => { if (!isConditionalMode) return; @@ -952,9 +995,15 @@ export function TableSectionRenderer({ baseColumn.selectOptions = dynamicSelectOptionsMap[col.field]; } + // 카테고리 타입인 경우 옵션 적용 및 select 타입으로 변환 + if (col.type === "category" && categoryOptionsMap[col.field]) { + baseColumn.type = "select"; // RepeaterTable에서 select로 렌더링 + baseColumn.selectOptions = categoryOptionsMap[col.field]; + } + return baseColumn; }); - }, [tableConfig.columns, dynamicSelectOptionsMap]); + }, [tableConfig.columns, dynamicSelectOptionsMap, categoryOptionsMap]); // 원본 계산 규칙 (조건부 계산 포함) const originalCalculationRules: TableCalculationRule[] = useMemo( diff --git a/frontend/lib/registry/components/universal-form-modal/UniversalFormModalConfigPanel.tsx b/frontend/lib/registry/components/universal-form-modal/UniversalFormModalConfigPanel.tsx index 8cf92430..9c74f970 100644 --- a/frontend/lib/registry/components/universal-form-modal/UniversalFormModalConfigPanel.tsx +++ b/frontend/lib/registry/components/universal-form-modal/UniversalFormModalConfigPanel.tsx @@ -308,12 +308,29 @@ export function UniversalFormModalConfigPanel({ column_comment?: string; inputType?: string; input_type?: string; - }) => ({ - name: c.columnName || c.column_name || "", - type: c.dataType || c.data_type || "text", - label: c.displayName || c.columnComment || c.column_comment || c.columnName || c.column_name || "", - inputType: c.inputType || c.input_type || "text", - }), + isNullable?: string; + is_nullable?: string; + }) => { + const colName = c.columnName || c.column_name || ""; + const dataType = c.dataType || c.data_type || "text"; + const inputType = c.inputType || c.input_type || "text"; + const displayName = c.displayName || c.columnComment || c.column_comment || colName; + const isNullable = c.isNullable || c.is_nullable || "YES"; + + return { + // camelCase (기존 호환성) + name: colName, + type: dataType, + label: displayName, + inputType: inputType, + // snake_case (TableSectionSettingsModal 호환성) + column_name: colName, + data_type: dataType, + is_nullable: isNullable, + comment: displayName, + input_type: inputType, + }; + }, ), })); } diff --git a/frontend/lib/registry/components/universal-form-modal/modals/TableColumnSettingsModal.tsx b/frontend/lib/registry/components/universal-form-modal/modals/TableColumnSettingsModal.tsx index 79a78d10..7099e43e 100644 --- a/frontend/lib/registry/components/universal-form-modal/modals/TableColumnSettingsModal.tsx +++ b/frontend/lib/registry/components/universal-form-modal/modals/TableColumnSettingsModal.tsx @@ -48,12 +48,12 @@ interface TableColumnSettingsModalProps { onOpenChange: (open: boolean) => void; column: TableColumnConfig; sourceTableName: string; // 소스 테이블명 - sourceTableColumns: { column_name: string; data_type: string; comment?: string }[]; + sourceTableColumns: { column_name: string; data_type: string; comment?: string; input_type?: string }[]; formFields: { columnName: string; label: string; sectionId?: string; sectionTitle?: string }[]; // formData 필드 목록 (섹션 정보 포함) sections: { id: string; title: string }[]; // 섹션 목록 onSave: (updatedColumn: TableColumnConfig) => void; tables: { table_name: string; comment?: string }[]; - tableColumns: Record; + tableColumns: Record; onLoadTableColumns: (tableName: string) => void; } @@ -103,6 +103,18 @@ export function TableColumnSettingsModal({ return tableColumns[externalTableName] || []; }, [tableColumns, externalTableName]); + // 소스 필드 기준으로 카테고리 타입인지 확인 + const actualSourceField = localColumn.sourceField || localColumn.field; + const sourceColumnInfo = sourceTableColumns.find((c) => c.column_name === actualSourceField); + const isCategoryColumn = sourceColumnInfo?.input_type === "category"; + + // 카테고리 컬럼인 경우 타입을 자동으로 category로 설정 + useEffect(() => { + if (isCategoryColumn && localColumn.type !== "category") { + updateColumn({ type: "category" }); + } + }, [isCategoryColumn, localColumn.type]); + // 컬럼 업데이트 함수 const updateColumn = (updates: Partial) => { setLocalColumn((prev) => ({ ...prev, ...updates })); @@ -574,10 +586,11 @@ export function TableColumnSettingsModal({
+ {isCategoryColumn && ( +

테이블 타입 관리에서 카테고리로 설정됨

+ )}
diff --git a/frontend/lib/registry/components/universal-form-modal/modals/TableSectionSettingsModal.tsx b/frontend/lib/registry/components/universal-form-modal/modals/TableSectionSettingsModal.tsx index a145b49a..1970f1a5 100644 --- a/frontend/lib/registry/components/universal-form-modal/modals/TableSectionSettingsModal.tsx +++ b/frontend/lib/registry/components/universal-form-modal/modals/TableSectionSettingsModal.tsx @@ -706,15 +706,15 @@ interface ColumnSettingItemProps { col: TableColumnConfig; index: number; totalCount: number; - saveTableColumns: { column_name: string; data_type: string; is_nullable: string; comment?: string }[]; + saveTableColumns: { column_name: string; data_type: string; is_nullable: string; comment?: string; input_type?: string }[]; displayColumns: string[]; // 검색 설정에서 선택한 표시 컬럼 목록 - sourceTableColumns: { column_name: string; data_type: string; is_nullable: string; comment?: string }[]; // 소스 테이블 컬럼 + sourceTableColumns: { column_name: string; data_type: string; is_nullable: string; comment?: string; input_type?: string }[]; // 소스 테이블 컬럼 sourceTableName: string; // 소스 테이블명 - externalTableColumns: { column_name: string; data_type: string; is_nullable: string; comment?: string }[]; // 외부 데이터 테이블 컬럼 + externalTableColumns: { column_name: string; data_type: string; is_nullable: string; comment?: string; input_type?: string }[]; // 외부 데이터 테이블 컬럼 externalTableName?: string; // 외부 데이터 테이블명 externalDataEnabled?: boolean; // 외부 데이터 소스 활성화 여부 tables: { table_name: string; comment?: string }[]; // 전체 테이블 목록 - tableColumns: Record; // 테이블별 컬럼 + tableColumns: Record; // 테이블별 컬럼 sections: { id: string; title: string }[]; // 섹션 목록 formFields: { columnName: string; label: string; sectionId?: string }[]; // 폼 필드 목록 tableConfig: TableSectionConfig; // 현재 행 필드 목록 표시용 @@ -755,6 +755,18 @@ function ColumnSettingItem({ const [parentFieldSearchOpen, setParentFieldSearchOpen] = useState(false); const [lookupTableOpenMap, setLookupTableOpenMap] = useState>({}); + // 소스 필드 기준으로 카테고리 타입인지 확인 + const actualSourceField = col.sourceField || col.field; + const sourceColumnInfo = sourceTableColumns.find((c) => c.column_name === actualSourceField); + const isCategoryColumn = sourceColumnInfo?.input_type === "category"; + + // 카테고리 컬럼인 경우 타입을 자동으로 category로 설정 + useEffect(() => { + if (isCategoryColumn && col.type !== "category") { + onUpdate({ type: "category" }); + } + }, [isCategoryColumn, col.type, onUpdate]); + // 조회 옵션 추가 const addLookupOption = () => { const newOption: LookupOption = { @@ -1117,8 +1129,12 @@ function ColumnSettingItem({ {/* 타입 */}
- onUpdate({ type: value })} + disabled={isCategoryColumn} + > + @@ -1129,6 +1145,9 @@ function ColumnSettingItem({ ))} + {isCategoryColumn && ( +

테이블 타입 관리에서 카테고리로 설정됨

+ )}
{/* 너비 */} diff --git a/frontend/lib/registry/components/universal-form-modal/types.ts b/frontend/lib/registry/components/universal-form-modal/types.ts index 25d04ea8..c5ecb1cc 100644 --- a/frontend/lib/registry/components/universal-form-modal/types.ts +++ b/frontend/lib/registry/components/universal-form-modal/types.ts @@ -899,6 +899,7 @@ export const TABLE_COLUMN_TYPE_OPTIONS = [ { value: "number", label: "숫자" }, { value: "date", label: "날짜" }, { value: "select", label: "선택(드롭다운)" }, + { value: "category", label: "카테고리" }, ] as const; // 값 매핑 타입 옵션 -- 2.43.0