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 = ({ -

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

+

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

= ({ -

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

+

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

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

추가 필터

- -
- -