diff --git a/backend-node/src/controllers/adminController.ts b/backend-node/src/controllers/adminController.ts index a28712c1..c8e8ce82 100644 --- a/backend-node/src/controllers/adminController.ts +++ b/backend-node/src/controllers/adminController.ts @@ -3245,6 +3245,7 @@ export const resetUserPassword = async ( /** * 테이블 스키마 조회 (엑셀 업로드 컬럼 매핑용) + * column_labels 테이블에서 라벨 정보도 함께 가져옴 */ export async function getTableSchema( req: AuthenticatedRequest, @@ -3264,20 +3265,25 @@ export async function getTableSchema( logger.info("테이블 스키마 조회", { tableName, companyCode }); - // information_schema에서 컬럼 정보 가져오기 + // information_schema와 column_labels를 JOIN하여 컬럼 정보와 라벨 정보 함께 가져오기 const schemaQuery = ` SELECT - column_name, - data_type, - is_nullable, - column_default, - character_maximum_length, - numeric_precision, - numeric_scale - FROM information_schema.columns - WHERE table_schema = 'public' - AND table_name = $1 - ORDER BY ordinal_position + ic.column_name, + ic.data_type, + ic.is_nullable, + ic.column_default, + ic.character_maximum_length, + ic.numeric_precision, + ic.numeric_scale, + cl.column_label, + cl.display_order + FROM information_schema.columns ic + LEFT JOIN column_labels cl + ON cl.table_name = ic.table_name + AND cl.column_name = ic.column_name + WHERE ic.table_schema = 'public' + AND ic.table_name = $1 + ORDER BY COALESCE(cl.display_order, ic.ordinal_position), ic.ordinal_position `; const columns = await query(schemaQuery, [tableName]); @@ -3290,9 +3296,10 @@ export async function getTableSchema( return; } - // 컬럼 정보를 간단한 형태로 변환 + // 컬럼 정보를 간단한 형태로 변환 (라벨 정보 포함) const columnList = columns.map((col: any) => ({ name: col.column_name, + label: col.column_label || col.column_name, // 라벨이 없으면 컬럼명 사용 type: col.data_type, nullable: col.is_nullable === "YES", default: col.column_default, diff --git a/frontend/app/(main)/screens/[screenId]/page.tsx b/frontend/app/(main)/screens/[screenId]/page.tsx index 8510d627..dffbd75b 100644 --- a/frontend/app/(main)/screens/[screenId]/page.tsx +++ b/frontend/app/(main)/screens/[screenId]/page.tsx @@ -18,10 +18,11 @@ import { DynamicComponentRenderer } from "@/lib/registry/DynamicComponentRendere import { ScreenPreviewProvider } from "@/contexts/ScreenPreviewContext"; import { useAuth } from "@/hooks/useAuth"; // 🆕 사용자 정보 import { useResponsive } from "@/lib/hooks/useResponsive"; // 🆕 반응형 감지 -import { TableOptionsProvider } from "@/contexts/TableOptionsContext"; // 🆕 테이블 옵션 -import { TableSearchWidgetHeightProvider, useTableSearchWidgetHeight } from "@/contexts/TableSearchWidgetHeightContext"; // 🆕 높이 관리 -import { ScreenContextProvider } from "@/contexts/ScreenContext"; // 🆕 컴포넌트 간 통신 -import { SplitPanelProvider } from "@/lib/registry/components/split-panel-layout/SplitPanelContext"; // 🆕 분할 패널 리사이즈 +import { TableOptionsProvider } from "@/contexts/TableOptionsContext"; // 테이블 옵션 +import { TableSearchWidgetHeightProvider, useTableSearchWidgetHeight } from "@/contexts/TableSearchWidgetHeightContext"; // 높이 관리 +import { ScreenContextProvider } from "@/contexts/ScreenContext"; // 컴포넌트 간 통신 +import { SplitPanelProvider } from "@/lib/registry/components/split-panel-layout/SplitPanelContext"; // 분할 패널 리사이즈 +import { ActiveTabProvider } from "@/contexts/ActiveTabContext"; // 활성 탭 관리 function ScreenViewPage() { const params = useParams(); @@ -307,7 +308,8 @@ function ScreenViewPage() { return ( - + +
{/* 레이아웃 준비 중 로딩 표시 */} {!layoutReady && ( @@ -786,7 +788,8 @@ function ScreenViewPage() { }} />
-
+
+
); } diff --git a/frontend/components/common/ExcelUploadModal.tsx b/frontend/components/common/ExcelUploadModal.tsx index 0f080bcc..a4a17274 100644 --- a/frontend/components/common/ExcelUploadModal.tsx +++ b/frontend/components/common/ExcelUploadModal.tsx @@ -29,7 +29,6 @@ import { Plus, Minus, ArrowRight, - Save, Zap, } from "lucide-react"; import { importFromExcel, getExcelSheetNames } from "@/lib/utils/excelExport"; @@ -52,12 +51,6 @@ interface ColumnMapping { systemColumn: string | null; } -interface UploadConfig { - name: string; - type: string; - mappings: ColumnMapping[]; -} - export const ExcelUploadModal: React.FC = ({ open, onOpenChange, @@ -88,8 +81,6 @@ export const ExcelUploadModal: React.FC = ({ const [excelColumns, setExcelColumns] = useState([]); const [systemColumns, setSystemColumns] = useState([]); const [columnMappings, setColumnMappings] = useState([]); - const [configName, setConfigName] = useState(""); - const [configType, setConfigType] = useState(""); // 4단계: 확인 const [isUploading, setIsUploading] = useState(false); @@ -114,7 +105,7 @@ export const ExcelUploadModal: React.FC = ({ const data = await importFromExcel(selectedFile, sheets[0]); setAllData(data); - setDisplayData(data.slice(0, 10)); + setDisplayData(data); // 전체 데이터를 미리보기에 표시 (스크롤 가능) if (data.length > 0) { const columns = Object.keys(data[0]); @@ -139,7 +130,7 @@ export const ExcelUploadModal: React.FC = ({ try { const data = await importFromExcel(file, sheetName); setAllData(data); - setDisplayData(data.slice(0, 10)); + setDisplayData(data); // 전체 데이터를 미리보기에 표시 (스크롤 가능) if (data.length > 0) { const columns = Object.keys(data[0]); @@ -236,13 +227,23 @@ export const ExcelUploadModal: React.FC = ({ } }; - // 자동 매핑 + // 자동 매핑 - 컬럼명과 라벨 모두 비교 const handleAutoMapping = () => { const newMappings = excelColumns.map((excelCol) => { - const matchedSystemCol = systemColumns.find( - (sysCol) => sysCol.name.toLowerCase() === excelCol.toLowerCase() + const normalizedExcelCol = excelCol.toLowerCase().trim(); + + // 1. 먼저 라벨로 매칭 시도 + let matchedSystemCol = systemColumns.find( + (sysCol) => sysCol.label && sysCol.label.toLowerCase().trim() === normalizedExcelCol ); + // 2. 라벨로 매칭되지 않으면 컬럼명으로 매칭 시도 + if (!matchedSystemCol) { + matchedSystemCol = systemColumns.find( + (sysCol) => sysCol.name.toLowerCase().trim() === normalizedExcelCol + ); + } + return { excelColumn: excelCol, systemColumn: matchedSystemCol ? matchedSystemCol.name : null, @@ -265,28 +266,6 @@ export const ExcelUploadModal: React.FC = ({ ); }; - // 설정 저장 - const handleSaveConfig = () => { - if (!configName.trim()) { - toast.error("거래처명을 입력해주세요."); - return; - } - - const config: UploadConfig = { - name: configName, - type: configType, - mappings: columnMappings, - }; - - const savedConfigs = JSON.parse( - localStorage.getItem("excelUploadConfigs") || "[]" - ); - savedConfigs.push(config); - localStorage.setItem("excelUploadConfigs", JSON.stringify(savedConfigs)); - - toast.success("설정이 저장되었습니다."); - }; - // 다음 단계 const handleNext = () => { if (currentStep === 1 && !file) { @@ -317,7 +296,8 @@ export const ExcelUploadModal: React.FC = ({ setIsUploading(true); try { - const mappedData = displayData.map((row) => { + // allData를 사용하여 전체 데이터 업로드 (displayData는 미리보기용 10개만) + const mappedData = allData.map((row) => { const mappedRow: Record = {}; columnMappings.forEach((mapping) => { if (mapping.systemColumn) { @@ -379,8 +359,6 @@ export const ExcelUploadModal: React.FC = ({ setExcelColumns([]); setSystemColumns([]); setColumnMappings([]); - setConfigName(""); - setConfigType(""); } }, [open]); @@ -689,27 +667,25 @@ export const ExcelUploadModal: React.FC = ({ )} - {/* 3단계: 컬럼 매핑 - 3단 레이아웃 */} + {/* 3단계: 컬럼 매핑 */} {currentStep === 3 && ( -
- {/* 왼쪽: 컬럼 매핑 설정 제목 + 자동 매핑 버튼 */} -
-
-

컬럼 매핑 설정

- -
+
+ {/* 상단: 제목 + 자동 매핑 버튼 */} +
+

컬럼 매핑 설정

+
- {/* 중앙: 매핑 리스트 */} + {/* 매핑 리스트 */}
엑셀 컬럼
@@ -734,7 +710,14 @@ export const ExcelUploadModal: React.FC = ({ } > - + + {mapping.systemColumn + ? (() => { + const col = systemColumns.find(c => c.name === mapping.systemColumn); + return col?.label || mapping.systemColumn; + })() + : "매핑 안함"} + @@ -746,7 +729,7 @@ export const ExcelUploadModal: React.FC = ({ value={col.name} className="text-xs sm:text-sm" > - {col.name} ({col.type}) + {col.label || col.name} ({col.type}) ))} @@ -755,50 +738,6 @@ export const ExcelUploadModal: React.FC = ({ ))}
- - {/* 오른쪽: 현재 설정 저장 */} -
-
- -

현재 설정 저장

-
-
-
- - setConfigName(e.target.value)} - placeholder="거래처 선택" - className="mt-1 h-8 text-xs sm:h-10 sm:text-sm" - /> -
-
- - setConfigType(e.target.value)} - placeholder="유형을 입력하세요 (예: 원자재)" - className="mt-1 h-8 text-xs sm:h-10 sm:text-sm" - /> -
- -
-
)} @@ -815,7 +754,7 @@ export const ExcelUploadModal: React.FC = ({ 시트: {selectedSheet}

- 데이터 행: {displayData.length}개 + 데이터 행: {allData.length}개

테이블: {tableName} diff --git a/frontend/components/common/ScreenModal.tsx b/frontend/components/common/ScreenModal.tsx index 811249a7..cceadae9 100644 --- a/frontend/components/common/ScreenModal.tsx +++ b/frontend/components/common/ScreenModal.tsx @@ -12,6 +12,7 @@ import { useAuth } from "@/hooks/useAuth"; import { TableOptionsProvider } from "@/contexts/TableOptionsContext"; import { TableSearchWidgetHeightProvider } from "@/contexts/TableSearchWidgetHeightContext"; import { useSplitPanelContext } from "@/contexts/SplitPanelContext"; +import { ActiveTabProvider } from "@/contexts/ActiveTabContext"; interface ScreenModalState { isOpen: boolean; @@ -666,6 +667,7 @@ export const ScreenModal: React.FC = ({ className }) => {

) : screenData ? ( +
= ({ className }) => { })}
+
) : (

화면 데이터가 없습니다.

diff --git a/frontend/components/screen/InteractiveScreenViewer.tsx b/frontend/components/screen/InteractiveScreenViewer.tsx index 480b3ddd..376f9953 100644 --- a/frontend/components/screen/InteractiveScreenViewer.tsx +++ b/frontend/components/screen/InteractiveScreenViewer.tsx @@ -51,6 +51,7 @@ import { useScreenPreview } from "@/contexts/ScreenPreviewContext"; import { TableOptionsProvider } from "@/contexts/TableOptionsContext"; import { TableOptionsToolbar } from "./table-options/TableOptionsToolbar"; import { SplitPanelProvider } from "@/lib/registry/components/split-panel-layout/SplitPanelContext"; +import { ActiveTabProvider } from "@/contexts/ActiveTabContext"; /** * 🔗 연쇄 드롭다운 래퍼 컴포넌트 @@ -2103,7 +2104,8 @@ export const InteractiveScreenViewer: React.FC = ( return ( - + +
{/* 테이블 옵션 툴바 */} @@ -2210,7 +2212,8 @@ export const InteractiveScreenViewer: React.FC = (
-
+
+
); }; diff --git a/frontend/components/screen/InteractiveScreenViewerDynamic.tsx b/frontend/components/screen/InteractiveScreenViewerDynamic.tsx index 5b09b092..4763507e 100644 --- a/frontend/components/screen/InteractiveScreenViewerDynamic.tsx +++ b/frontend/components/screen/InteractiveScreenViewerDynamic.tsx @@ -39,22 +39,25 @@ interface InteractiveScreenViewerProps { id: number; tableName?: string; }; - menuObjid?: number; // 🆕 메뉴 OBJID (코드 스코프용) + menuObjid?: number; // 메뉴 OBJID (코드 스코프용) onSave?: () => Promise; onRefresh?: () => void; onFlowRefresh?: () => void; - // 🆕 외부에서 전달받는 사용자 정보 (ScreenModal 등에서 사용) + // 외부에서 전달받는 사용자 정보 (ScreenModal 등에서 사용) userId?: string; userName?: string; companyCode?: string; - // 🆕 그룹 데이터 (EditModal에서 전달) + // 그룹 데이터 (EditModal에서 전달) groupedData?: Record[]; - // 🆕 비활성화할 필드 목록 (EditModal에서 전달) + // 비활성화할 필드 목록 (EditModal에서 전달) disabledFields?: string[]; - // 🆕 EditModal 내부인지 여부 (button-primary가 EditModal의 handleSave 사용하도록) + // EditModal 내부인지 여부 (button-primary가 EditModal의 handleSave 사용하도록) isInModal?: boolean; - // 🆕 원본 데이터 (수정 모드에서 UPDATE 판단용) + // 원본 데이터 (수정 모드에서 UPDATE 판단용) originalData?: Record | null; + // 탭 관련 정보 (탭 내부의 컴포넌트에서 사용) + parentTabId?: string; // 부모 탭 ID + parentTabsComponentId?: string; // 부모 탭 컴포넌트 ID } export const InteractiveScreenViewerDynamic: React.FC = ({ @@ -74,7 +77,9 @@ export const InteractiveScreenViewerDynamic: React.FC { const { isPreviewMode } = useScreenPreview(); // 프리뷰 모드 확인 const { userName: authUserName, user: authUser } = useAuth(); @@ -359,43 +364,43 @@ export const InteractiveScreenViewerDynamic: React.FC { - console.log("🔍 테이블에서 선택된 행 데이터:", selectedData); + console.log("테이블에서 선택된 행 데이터:", selectedData); setSelectedRowsData(selectedData); }} - // 🆕 그룹 데이터 전달 (EditModal → ModalRepeaterTable) groupedData={groupedData} - // 🆕 비활성화 필드 전달 (EditModal → 각 컴포넌트) disabledFields={disabledFields} flowSelectedData={flowSelectedData} flowSelectedStepId={flowSelectedStepId} onFlowSelectedDataChange={(selectedData, stepId) => { - console.log("🔍 플로우에서 선택된 데이터:", { selectedData, stepId }); + console.log("플로우에서 선택된 데이터:", { selectedData, stepId }); setFlowSelectedData(selectedData); setFlowSelectedStepId(stepId); }} onRefresh={ onRefresh || (() => { - // 부모로부터 전달받은 onRefresh 또는 기본 동작 - console.log("🔄 InteractiveScreenViewerDynamic onRefresh 호출"); + console.log("InteractiveScreenViewerDynamic onRefresh 호출"); }) } onFlowRefresh={onFlowRefresh} onClose={() => { // buttonActions.ts가 이미 처리함 }} + // 탭 관련 정보 전달 + parentTabId={parentTabId} + parentTabsComponentId={parentTabsComponentId} /> ); } diff --git a/frontend/components/screen/ScreenDesigner.tsx b/frontend/components/screen/ScreenDesigner.tsx index 3a440f07..d4cab3cf 100644 --- a/frontend/components/screen/ScreenDesigner.tsx +++ b/frontend/components/screen/ScreenDesigner.tsx @@ -958,6 +958,10 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD characterMaximumLength: col.characterMaximumLength || col.character_maximum_length, codeCategory: col.codeCategory || col.code_category, codeValue: col.codeValue || col.code_value, + // 엔티티 타입용 참조 테이블 정보 + referenceTable: col.referenceTable || col.reference_table, + referenceColumn: col.referenceColumn || col.reference_column, + displayColumn: col.displayColumn || col.display_column, }; }); diff --git a/frontend/components/screen/config-panels/EntityConfigPanel.tsx b/frontend/components/screen/config-panels/EntityConfigPanel.tsx index edb278f2..83773500 100644 --- a/frontend/components/screen/config-panels/EntityConfigPanel.tsx +++ b/frontend/components/screen/config-panels/EntityConfigPanel.tsx @@ -6,18 +6,10 @@ import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Switch } from "@/components/ui/switch"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; -import { Button } from "@/components/ui/button"; -import { Textarea } from "@/components/ui/textarea"; -import { Database, Search, Plus, Trash2 } from "lucide-react"; +import { Database, Search, Info } from "lucide-react"; import { WebTypeConfigPanelProps } from "@/lib/registry/types"; import { WidgetComponent, EntityTypeConfig } from "@/types/screen"; - -interface EntityField { - name: string; - label: string; - type: string; - visible: boolean; -} +import { tableTypeApi } from "@/lib/api/screen"; export const EntityConfigPanel: React.FC = ({ component, @@ -27,16 +19,31 @@ export const EntityConfigPanel: React.FC = ({ const widget = component as WidgetComponent; const config = (widget.webTypeConfig as EntityTypeConfig) || {}; - // 로컬 상태 + // 테이블 타입 관리에서 설정된 참조 테이블 정보 + const [referenceInfo, setReferenceInfo] = useState<{ + referenceTable: string; + referenceColumn: string; + displayColumn: string; + isLoading: boolean; + error: string | null; + }>({ + referenceTable: "", + referenceColumn: "", + displayColumn: "", + isLoading: true, + error: null, + }); + + // 로컬 상태 (UI 관련 설정만) const [localConfig, setLocalConfig] = useState({ entityType: config.entityType || "", displayFields: config.displayFields || [], searchFields: config.searchFields || [], - valueField: config.valueField || "id", - labelField: config.labelField || "name", + valueField: config.valueField || "", + labelField: config.labelField || "", multiple: config.multiple || false, - searchable: config.searchable !== false, // 기본값 true - placeholder: config.placeholder || "엔티티를 선택하세요", + searchable: config.searchable !== false, + placeholder: config.placeholder || "항목을 선택하세요", emptyMessage: config.emptyMessage || "검색 결과가 없습니다", pageSize: config.pageSize || 20, minSearchLength: config.minSearchLength || 1, @@ -47,10 +54,95 @@ export const EntityConfigPanel: React.FC = ({ filters: config.filters || {}, }); - // 새 필드 추가용 상태 - const [newFieldName, setNewFieldName] = useState(""); - const [newFieldLabel, setNewFieldLabel] = useState(""); - const [newFieldType, setNewFieldType] = useState("string"); + // 테이블 타입 관리에서 설정된 참조 테이블 정보 로드 + useEffect(() => { + const loadReferenceInfo = async () => { + // 컴포넌트의 테이블명과 컬럼명이 있는 경우에만 조회 + const tableName = widget.tableName; + const columnName = widget.columnName; + + if (!tableName || !columnName) { + setReferenceInfo({ + referenceTable: "", + referenceColumn: "", + displayColumn: "", + isLoading: false, + error: "테이블 또는 컬럼 정보가 없습니다.", + }); + return; + } + + try { + // 테이블 타입 관리에서 컬럼 정보 조회 + const columns = await tableTypeApi.getColumns(tableName); + const columnInfo = columns.find((col: any) => + (col.columnName || col.column_name) === columnName + ); + + if (columnInfo) { + const refTable = columnInfo.referenceTable || columnInfo.reference_table || ""; + const refColumn = columnInfo.referenceColumn || columnInfo.reference_column || ""; + const dispColumn = columnInfo.displayColumn || columnInfo.display_column || ""; + + // detailSettings에서도 정보 확인 (JSON 파싱) + let detailSettings: any = {}; + if (columnInfo.detailSettings) { + try { + if (typeof columnInfo.detailSettings === 'string') { + detailSettings = JSON.parse(columnInfo.detailSettings); + } else { + detailSettings = columnInfo.detailSettings; + } + } catch { + // JSON 파싱 실패 시 무시 + } + } + + const finalRefTable = refTable || detailSettings.referenceTable || ""; + const finalRefColumn = refColumn || detailSettings.referenceColumn || ""; + const finalDispColumn = dispColumn || detailSettings.displayColumn || ""; + + setReferenceInfo({ + referenceTable: finalRefTable, + referenceColumn: finalRefColumn, + displayColumn: finalDispColumn, + isLoading: false, + error: null, + }); + + // webTypeConfig에 참조 테이블 정보 자동 설정 + if (finalRefTable) { + const newConfig = { + ...localConfig, + valueField: finalRefColumn || "id", + labelField: finalDispColumn || "name", + }; + setLocalConfig(newConfig); + onUpdateProperty("webTypeConfig", newConfig); + } + } else { + setReferenceInfo({ + referenceTable: "", + referenceColumn: "", + displayColumn: "", + isLoading: false, + error: "컬럼 정보를 찾을 수 없습니다.", + }); + } + } catch (error) { + console.error("참조 테이블 정보 로드 실패:", error); + setReferenceInfo({ + referenceTable: "", + referenceColumn: "", + displayColumn: "", + isLoading: false, + error: "참조 테이블 정보 로드 실패", + }); + } + }; + + loadReferenceInfo(); + }, [widget.tableName, widget.columnName]); // 컴포넌트 변경 시 로컬 상태 동기화 useEffect(() => { @@ -59,11 +151,11 @@ export const EntityConfigPanel: React.FC = ({ entityType: currentConfig.entityType || "", displayFields: currentConfig.displayFields || [], searchFields: currentConfig.searchFields || [], - valueField: currentConfig.valueField || "id", - labelField: currentConfig.labelField || "name", + valueField: currentConfig.valueField || referenceInfo.referenceColumn || "", + labelField: currentConfig.labelField || referenceInfo.displayColumn || "", multiple: currentConfig.multiple || false, searchable: currentConfig.searchable !== false, - placeholder: currentConfig.placeholder || "엔티티를 선택하세요", + placeholder: currentConfig.placeholder || "항목을 선택하세요", emptyMessage: currentConfig.emptyMessage || "검색 결과가 없습니다", pageSize: currentConfig.pageSize || 20, minSearchLength: currentConfig.minSearchLength || 1, @@ -73,7 +165,7 @@ export const EntityConfigPanel: React.FC = ({ apiEndpoint: currentConfig.apiEndpoint || "", filters: currentConfig.filters || {}, }); - }, [widget.webTypeConfig]); + }, [widget.webTypeConfig, referenceInfo.referenceColumn, referenceInfo.displayColumn]); // 설정 업데이트 핸들러 (즉시 부모에게 전달 - 드롭다운, 체크박스 등) const updateConfig = (field: keyof EntityTypeConfig, value: any) => { @@ -92,89 +184,6 @@ export const EntityConfigPanel: React.FC = ({ onUpdateProperty("webTypeConfig", localConfig); }; - // 필드 추가 - const addDisplayField = () => { - if (!newFieldName.trim() || !newFieldLabel.trim()) return; - - const newField: EntityField = { - name: newFieldName.trim(), - label: newFieldLabel.trim(), - type: newFieldType, - visible: true, - }; - - const newFields = [...localConfig.displayFields, newField]; - updateConfig("displayFields", newFields); - setNewFieldName(""); - setNewFieldLabel(""); - setNewFieldType("string"); - }; - - // 필드 제거 - const removeDisplayField = (index: number) => { - const newFields = localConfig.displayFields.filter((_, i) => i !== index); - updateConfig("displayFields", newFields); - }; - - // 필드 업데이트 (입력 중) - 로컬 상태만 업데이트 - const updateDisplayField = (index: number, field: keyof EntityField, value: any) => { - const newFields = [...localConfig.displayFields]; - newFields[index] = { ...newFields[index], [field]: value }; - setLocalConfig({ ...localConfig, displayFields: newFields }); - }; - - // 필드 업데이트 완료 (onBlur) - 부모에게 전달 - const handleFieldBlur = () => { - onUpdateProperty("webTypeConfig", localConfig); - }; - - // 검색 필드 토글 - const toggleSearchField = (fieldName: string) => { - const currentSearchFields = localConfig.searchFields || []; - const newSearchFields = currentSearchFields.includes(fieldName) - ? currentSearchFields.filter((f) => f !== fieldName) - : [...currentSearchFields, fieldName]; - updateConfig("searchFields", newSearchFields); - }; - - // 기본 엔티티 타입들 - const commonEntityTypes = [ - { value: "user", label: "사용자", fields: ["id", "name", "email", "department"] }, - { value: "department", label: "부서", fields: ["id", "name", "code", "parentId"] }, - { value: "product", label: "제품", fields: ["id", "name", "code", "category", "price"] }, - { value: "customer", label: "고객", fields: ["id", "name", "company", "contact"] }, - { value: "project", label: "프로젝트", fields: ["id", "name", "status", "manager", "startDate"] }, - ]; - - // 기본 엔티티 타입 적용 - const applyEntityType = (entityType: string) => { - const entityConfig = commonEntityTypes.find((e) => e.value === entityType); - if (!entityConfig) return; - - updateConfig("entityType", entityType); - updateConfig("apiEndpoint", `/api/entities/${entityType}`); - - const defaultFields: EntityField[] = entityConfig.fields.map((field) => ({ - name: field, - label: field.charAt(0).toUpperCase() + field.slice(1), - type: field.includes("Date") ? "date" : field.includes("price") || field.includes("Id") ? "number" : "string", - visible: true, - })); - - updateConfig("displayFields", defaultFields); - updateConfig("searchFields", [entityConfig.fields[1] || "name"]); // 두 번째 필드를 기본 검색 필드로 - }; - - // 필드 타입 옵션 - const fieldTypes = [ - { value: "string", label: "문자열" }, - { value: "number", label: "숫자" }, - { value: "date", label: "날짜" }, - { value: "boolean", label: "불린" }, - { value: "email", label: "이메일" }, - { value: "url", label: "URL" }, - ]; - return ( @@ -182,12 +191,70 @@ export const EntityConfigPanel: React.FC = ({ 엔티티 설정 - 데이터베이스 엔티티 선택 필드의 설정을 관리합니다. + + 데이터베이스 엔티티 선택 필드의 설정을 관리합니다. + - {/* 기본 설정 */} + {/* 참조 테이블 정보 (테이블 타입 관리에서 설정된 값 - 읽기 전용) */}
-

기본 설정

+

+ 참조 테이블 정보 + + 테이블 타입 관리에서 설정 + +

+ + {referenceInfo.isLoading ? ( +
+

참조 테이블 정보 로딩 중...

+
+ ) : referenceInfo.error ? ( +
+

+ + {referenceInfo.error} +

+

+ 테이블 타입 관리에서 이 컬럼의 참조 테이블을 먼저 설정해주세요. +

+
+ ) : !referenceInfo.referenceTable ? ( +
+

+ + 참조 테이블이 설정되지 않았습니다. +

+

+ 테이블 타입 관리에서 이 컬럼의 참조 테이블을 먼저 설정해주세요. +

+
+ ) : ( +
+
+
+ 참조 테이블: +
{referenceInfo.referenceTable}
+
+
+ 참조 컬럼: +
{referenceInfo.referenceColumn || "-"}
+
+
+ 표시 컬럼: +
{referenceInfo.displayColumn || "-"}
+
+
+

+ 이 정보는 테이블 타입 관리에서 변경할 수 있습니다. +

+
+ )} +
+ + {/* UI 모드 설정 */} +
+

UI 설정

{/* UI 모드 선택 */}
@@ -216,208 +283,6 @@ export const EntityConfigPanel: React.FC = ({

-
- - updateConfigLocal("entityType", e.target.value)} - onBlur={handleInputBlur} - placeholder="user, product, department..." - className="text-xs" - /> -
- -
- -
- {commonEntityTypes.map((entity) => ( - - ))} -
-
- -
- - updateConfigLocal("apiEndpoint", e.target.value)} - onBlur={handleInputBlur} - placeholder="/api/entities/user" - className="text-xs" - /> -
-
- - {/* 필드 매핑 */} -
-

필드 매핑

- -
-
- - updateConfigLocal("valueField", e.target.value)} - onBlur={handleInputBlur} - placeholder="id" - className="text-xs" - /> -
- -
- - updateConfigLocal("labelField", e.target.value)} - onBlur={handleInputBlur} - placeholder="name" - className="text-xs" - /> -
-
-
- - {/* 표시 필드 관리 */} -
-

표시 필드

- - {/* 새 필드 추가 */} -
- -
- setNewFieldName(e.target.value)} - placeholder="필드명" - className="flex-1 text-xs" - /> - setNewFieldLabel(e.target.value)} - placeholder="라벨" - className="flex-1 text-xs" - /> - - -
-
- - {/* 현재 필드 목록 */} -
- -
- {localConfig.displayFields.map((field, index) => ( -
- { - const newFields = [...localConfig.displayFields]; - newFields[index] = { ...newFields[index], visible: checked }; - const newConfig = { ...localConfig, displayFields: newFields }; - setLocalConfig(newConfig); - onUpdateProperty("webTypeConfig", newConfig); - }} - /> - updateDisplayField(index, "name", e.target.value)} - onBlur={handleFieldBlur} - placeholder="필드명" - className="flex-1 text-xs" - /> - updateDisplayField(index, "label", e.target.value)} - onBlur={handleFieldBlur} - placeholder="라벨" - className="flex-1 text-xs" - /> - - - -
- ))} -
-
-
- - {/* 검색 설정 */} -
-

검색 설정

-
@@ -445,6 +310,11 @@ export const EntityConfigPanel: React.FC = ({ className="text-xs" />
+
+ + {/* 검색 설정 */} +
+

검색 설정

@@ -483,7 +353,7 @@ export const EntityConfigPanel: React.FC = ({ -

엔티티를 검색할 수 있습니다.

+

항목을 검색할 수 있습니다.

= ({ -

여러 엔티티를 선택할 수 있습니다.

+

여러 항목을 선택할 수 있습니다.

= ({
- {/* 필터 설정 */} -
-

추가 필터

- -
- -