"use client"; import React, { useState, useEffect, useCallback, useMemo } from "react"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, } from "@/components/ui/command"; import { toast } from "sonner"; import { cn } from "@/lib/utils"; import { Badge } from "@/components/ui/badge"; import { Database, Link2, GitBranch, Columns3, Save, Plus, Pencil, Trash2, RefreshCw, Loader2, Check, ChevronsUpDown, } from "lucide-react"; import { getTableRelations, createTableRelation, updateTableRelation, deleteTableRelation, getFieldJoins, createFieldJoin, updateFieldJoin, deleteFieldJoin, getDataFlows, createDataFlow, updateDataFlow, deleteDataFlow, FieldJoin, DataFlow, TableRelation, } from "@/lib/api/screenGroup"; import { tableManagementApi, ColumnTypeInfo, TableInfo } from "@/lib/api/tableManagement"; // ============================================================ // 타입 정의 // ============================================================ // 기존 설정 정보 (화면 디자이너에서 추출) interface ExistingConfig { joinColumnRefs?: Array<{ column: string; refTable: string; refTableLabel?: string; refColumn: string; }>; filterColumns?: string[]; fieldMappings?: Array<{ targetField: string; sourceField: string; sourceTable?: string; sourceDisplayName?: string; }>; referencedBy?: Array<{ fromTable: string; fromTableLabel?: string; fromColumn: string; toColumn: string; toColumnLabel?: string; relationType: string; }>; columns?: Array<{ name: string; originalName?: string; type: string; isPrimaryKey?: boolean; isForeignKey?: boolean; }>; // 화면 노드용 테이블 정보 mainTable?: string; filterTables?: Array<{ tableName: string; tableLabel: string; filterColumns: string[]; joinColumnRefs: Array<{ column: string; refTable: string; refTableLabel?: string; refColumn: string; }>; }>; } interface NodeSettingModalProps { isOpen: boolean; onClose: () => void; // 노드 정보 nodeType: "screen" | "table"; nodeId: string; // 노드 ID (예: screen-1, table-sales_order_mng) screenId: number; screenName: string; tableName?: string; // 테이블 노드인 경우 tableLabel?: string; // 그룹 정보 (데이터 흐름 설정에 필요) groupId?: number; groupScreens?: Array<{ screen_id: number; screen_name: string }>; // 기존 설정 정보 (화면 디자이너에서 추출한 조인/필터 정보) existingConfig?: ExistingConfig; // 새로고침 콜백 onRefresh?: () => void; } // 탭 ID type TabId = "table-relation" | "join-setting" | "data-flow" | "field-mapping"; // ============================================================ // 검색 가능한 셀렉트 컴포넌트 // ============================================================ interface SearchableSelectProps { value: string; onValueChange: (value: string) => void; options: Array<{ value: string; label: string; description?: string }>; placeholder?: string; searchPlaceholder?: string; emptyText?: string; disabled?: boolean; className?: string; } function SearchableSelect({ value, onValueChange, options, placeholder = "선택", searchPlaceholder = "검색...", emptyText = "항목을 찾을 수 없습니다.", disabled = false, className, }: SearchableSelectProps) { const [open, setOpen] = useState(false); const selectedOption = options.find((opt) => opt.value === value); return ( {emptyText} {options.map((option) => ( { onValueChange(option.value); setOpen(false); }} className="text-xs" >
{option.label} {option.description && ( {option.description} )}
))}
); } // ============================================================ // 컴포넌트 // ============================================================ export default function NodeSettingModal({ isOpen, onClose, nodeType, nodeId, screenId, screenName, tableName, tableLabel, groupId, groupScreens = [], existingConfig, onRefresh, }: NodeSettingModalProps) { // 탭 상태 const [activeTab, setActiveTab] = useState("table-relation"); // 로딩 상태 const [loading, setLoading] = useState(false); // 테이블 목록 (조인/필터 설정용) const [tables, setTables] = useState([]); const [tableColumns, setTableColumns] = useState>({}); // 테이블 연결 데이터 const [tableRelations, setTableRelations] = useState([]); // 조인 설정 데이터 const [fieldJoins, setFieldJoins] = useState([]); // 데이터 흐름 데이터 const [dataFlows, setDataFlows] = useState([]); // ============================================================ // 데이터 로드 // ============================================================ // 테이블 목록 로드 const loadTables = useCallback(async () => { try { const response = await tableManagementApi.getTableList(); if (response.success && response.data) { setTables(response.data); } } catch (error) { console.error("테이블 목록 로드 실패:", error); } }, []); // 테이블 컬럼 로드 const loadTableColumns = useCallback(async (tblName: string) => { if (tableColumns[tblName]) return; // 이미 로드됨 try { const response = await tableManagementApi.getColumnList(tblName); if (response.success && response.data) { setTableColumns(prev => ({ ...prev, [tblName]: response.data?.columns || [], })); } } catch (error) { console.error(`테이블 컬럼 로드 실패 (${tblName}):`, error); } }, [tableColumns]); // 테이블 연결 로드 const loadTableRelations = useCallback(async () => { if (!screenId) return; setLoading(true); try { const response = await getTableRelations({ screen_id: screenId }); if (response.success && response.data) { setTableRelations(response.data); } } catch (error) { console.error("테이블 연결 로드 실패:", error); } finally { setLoading(false); } }, [screenId]); // 조인 설정 로드 const loadFieldJoins = useCallback(async () => { if (!screenId) return; setLoading(true); try { const response = await getFieldJoins(screenId); if (response.success && response.data) { setFieldJoins(response.data); } } catch (error) { console.error("조인 설정 로드 실패:", error); } finally { setLoading(false); } }, [screenId]); // 데이터 흐름 로드 const loadDataFlows = useCallback(async () => { if (!groupId) return; setLoading(true); try { const response = await getDataFlows(groupId); if (response.success && response.data) { // 현재 화면 관련 흐름만 필터링 const filtered = response.data.filter( flow => flow.source_screen_id === screenId || flow.target_screen_id === screenId ); setDataFlows(filtered); } } catch (error) { console.error("데이터 흐름 로드 실패:", error); } finally { setLoading(false); } }, [groupId, screenId]); // 모달 열릴 때 데이터 로드 useEffect(() => { if (isOpen) { loadTables(); loadTableRelations(); loadFieldJoins(); if (groupId) { loadDataFlows(); } // 현재 테이블 컬럼 로드 if (tableName) { loadTableColumns(tableName); } } }, [isOpen, loadTables, loadTableRelations, loadFieldJoins, loadDataFlows, tableName, groupId, loadTableColumns]); // ============================================================ // 이벤트 핸들러 // ============================================================ // 모달 닫기 const handleClose = () => { onClose(); }; // 새로고침 const handleRefresh = async () => { setLoading(true); try { await Promise.all([ loadTableRelations(), loadFieldJoins(), groupId ? loadDataFlows() : Promise.resolve(), ]); toast.success("데이터가 새로고침되었습니다."); } catch (error) { toast.error("새로고침 실패"); } finally { setLoading(false); } }; // ============================================================ // 렌더링 // ============================================================ // 모달 제목 const modalTitle = nodeType === "screen" ? `화면 설정: ${screenName}` : `테이블 설정: ${tableLabel || tableName}`; // 모달 설명 const modalDescription = nodeType === "screen" ? "화면의 테이블 연결, 조인, 데이터 흐름을 설정합니다." : "테이블의 조인 관계 및 필드 매핑을 설정합니다."; return ( {nodeType === "screen" ? ( ) : ( )} {modalTitle} {modalDescription}
setActiveTab(v as TabId)} className="h-full flex flex-col">
테이블 연결 연결 조인 설정 조인 데이터 흐름 흐름 필드 매핑 매핑
{/* 탭 컨텐츠 */}
{/* 탭1: 테이블 연결 */} {/* 탭2: 조인 설정 */} {/* 탭3: 데이터 흐름 */} {/* 탭4: 필드 매핑 */}
); } // ============================================================ // 탭1: 테이블 연결 설정 // ============================================================ interface TableRelationTabProps { screenId: number; screenName: string; tableRelations: TableRelation[]; tables: TableInfo[]; loading: boolean; onReload: () => void; onRefreshVisualization?: () => void; nodeType: "screen" | "table"; existingConfig?: ExistingConfig; } function TableRelationTab({ screenId, screenName, tableRelations, tables, loading, onReload, onRefreshVisualization, nodeType, existingConfig, }: TableRelationTabProps) { const [isEditing, setIsEditing] = useState(false); const [editItem, setEditItem] = useState(null); const [formData, setFormData] = useState({ table_name: "", relation_type: "main", crud_operations: "CR", description: "", is_active: "Y", }); // 폼 초기화 const resetForm = () => { setFormData({ table_name: "", relation_type: "main", crud_operations: "CR", description: "", is_active: "Y", }); setEditItem(null); setIsEditing(false); }; // 수정 모드 const handleEdit = (item: TableRelation) => { setEditItem(item); setFormData({ table_name: item.table_name, relation_type: item.relation_type, crud_operations: item.crud_operations, description: item.description || "", is_active: item.is_active, }); setIsEditing(true); }; // 저장 const handleSave = async () => { if (!formData.table_name) { toast.error("테이블을 선택해주세요."); return; } try { const payload = { screen_id: screenId, ...formData, }; let response; if (editItem) { response = await updateTableRelation(editItem.id, payload); } else { response = await createTableRelation(payload); } if (response.success) { toast.success(editItem ? "테이블 연결이 수정되었습니다." : "테이블 연결이 추가되었습니다."); resetForm(); onReload(); onRefreshVisualization?.(); } else { toast.error(response.message || "저장에 실패했습니다."); } } catch (error: any) { toast.error(error.message || "저장 중 오류가 발생했습니다."); } }; // 삭제 const handleDelete = async (id: number) => { if (!confirm("정말 삭제하시겠습니까?")) return; try { const response = await deleteTableRelation(id); if (response.success) { toast.success("테이블 연결이 삭제되었습니다."); onReload(); onRefreshVisualization?.(); } else { toast.error(response.message || "삭제에 실패했습니다."); } } catch (error: any) { toast.error(error.message || "삭제 중 오류가 발생했습니다."); } }; // 화면 디자이너에서 추출한 테이블 관계를 통합 목록으로 변환 const designerTableRelations = useMemo(() => { if (nodeType !== "screen" || !existingConfig) return []; const result: Array<{ id: string; source: "designer"; table_name: string; table_label?: string; relation_type: string; crud_operations: string; description: string; filterColumns?: string[]; joinColumnRefs?: Array<{ column: string; refTable: string; refTableLabel?: string; refColumn: string; }>; }> = []; // 메인 테이블 추가 if (existingConfig.mainTable) { result.push({ id: `designer-main-${existingConfig.mainTable}`, source: "designer", table_name: existingConfig.mainTable, table_label: existingConfig.mainTable, relation_type: "main", crud_operations: "CRUD", description: "화면의 주요 데이터 소스 테이블", }); } // 필터 테이블 추가 if (existingConfig.filterTables) { existingConfig.filterTables.forEach((ft, idx) => { result.push({ id: `designer-filter-${ft.tableName}-${idx}`, source: "designer", table_name: ft.tableName, table_label: ft.tableLabel, relation_type: "sub", crud_operations: "R", description: "마스터-디테일 필터 테이블", filterColumns: ft.filterColumns, joinColumnRefs: ft.joinColumnRefs, }); }); } return result; }, [nodeType, existingConfig]); // DB 테이블 관계와 디자이너 테이블 관계 통합 const unifiedTableRelations = useMemo(() => { // DB 관계 const dbRelations = tableRelations.map(item => ({ ...item, id: item.id, source: "db" as const, })); // 디자이너 관계 (DB에 이미 있는 테이블은 제외) const dbTableNames = new Set(tableRelations.map(r => r.table_name)); const filteredDesignerRelations = designerTableRelations.filter( dr => !dbTableNames.has(dr.table_name) ); return [...filteredDesignerRelations, ...dbRelations]; }, [tableRelations, designerTableRelations]); // 디자이너 항목 수정 (DB로 저장) const handleEditDesignerRelation = (item: typeof designerTableRelations[0]) => { setFormData({ table_name: item.table_name, relation_type: item.relation_type, crud_operations: item.crud_operations, description: item.description || "", is_active: "Y", }); setEditItem(null); setIsEditing(true); }; return (
{/* 입력 폼 */}
{isEditing ? "테이블 연결 수정" : "새 테이블 연결 추가"}
setFormData(prev => ({ ...prev, table_name: v }))} options={tables.map((t) => ({ value: t.tableName, label: t.displayName || t.tableName, description: t.tableName !== t.displayName ? t.tableName : undefined, }))} placeholder="테이블 선택" searchPlaceholder="테이블 검색..." />
setFormData(prev => ({ ...prev, relation_type: v }))} options={[ { value: "main", label: "메인 테이블" }, { value: "sub", label: "서브 테이블" }, { value: "lookup", label: "조회 테이블" }, { value: "save", label: "저장 테이블" }, ]} placeholder="관계 유형" searchPlaceholder="유형 검색..." />
setFormData(prev => ({ ...prev, crud_operations: v }))} options={[ { value: "C", label: "생성(C)" }, { value: "R", label: "읽기(R)" }, { value: "CR", label: "생성+읽기(CR)" }, { value: "CRU", label: "생성+읽기+수정(CRU)" }, { value: "CRUD", label: "전체(CRUD)" }, ]} placeholder="CRUD 권한" searchPlaceholder="권한 검색..." />
setFormData(prev => ({ ...prev, description: e.target.value }))} placeholder="설명 입력" className="h-9 text-xs" />
{isEditing && ( )}
{/* 목록 */}
출처 테이블 관계 유형 CRUD 설명 작업 {loading ? ( ) : unifiedTableRelations.length === 0 ? ( 등록된 테이블 연결이 없습니다. ) : ( unifiedTableRelations.map((item) => ( {item.source === "designer" ? "화면" : "DB"}
{item.table_label || item.table_name} {item.table_label && item.table_label !== item.table_name && ( ({item.table_name}) )}
{/* 필터 테이블의 경우 필터 컬럼/조인 정보 표시 */} {item.source === "designer" && "filterColumns" in item && item.filterColumns && item.filterColumns.length > 0 && (
{item.filterColumns.map((col, idx) => ( {col} ))}
)} {item.source === "designer" && "joinColumnRefs" in item && item.joinColumnRefs && item.joinColumnRefs.length > 0 && (
{item.joinColumnRefs.map((join, idx) => ( {join.column}→{join.refTable} ))}
)}
{item.relation_type === "main" ? "메인" : item.relation_type === "sub" ? "필터" : item.relation_type === "save" ? "저장" : item.relation_type === "lookup" ? "조회" : item.relation_type} {item.crud_operations} {item.description || "-"}
{item.source === "db" ? ( <> ) : ( )}
)) )}
); } // ============================================================ // 탭2: 조인 설정 // ============================================================ interface JoinSettingTabProps { screenId: number; tableName?: string; fieldJoins: FieldJoin[]; tables: TableInfo[]; tableColumns: Record; loading: boolean; onReload: () => void; onLoadColumns: (tableName: string) => void; onRefreshVisualization?: () => void; // 기존 설정 정보 (화면 디자이너에서 추출) existingConfig?: ExistingConfig; } // 화면 디자이너 조인 설정을 통합 형식으로 변환하기 위한 인터페이스 interface UnifiedJoinItem { id: number | string; // DB는 숫자, 화면 디자이너는 문자열 source: "db" | "designer"; // 출처 save_table: string; save_table_label?: string; save_column: string; join_table: string; join_table_label?: string; join_column: string; display_column?: string; join_type: string; } function JoinSettingTab({ screenId, tableName, fieldJoins, tables, tableColumns, loading, onReload, onLoadColumns, onRefreshVisualization, existingConfig, }: JoinSettingTabProps) { const [isEditing, setIsEditing] = useState(false); const [editItem, setEditItem] = useState(null); const [editingDesignerItem, setEditingDesignerItem] = useState(null); const [formData, setFormData] = useState({ field_name: "", save_table: tableName || "", save_column: "", join_table: "", join_column: "", display_column: "", join_type: "LEFT", filter_condition: "", is_active: "Y", }); // 테이블 라벨 가져오기 (tableName -> displayName) - 먼저 선언해야 함 const tableLabel = tables.find(t => t.tableName === tableName)?.displayName; // 화면 디자이너 조인 설정을 통합 형식으로 변환 // 1. 현재 테이블의 조인 설정 const directJoins: UnifiedJoinItem[] = (existingConfig?.joinColumnRefs || []).map((ref, idx) => ({ id: `designer-direct-${idx}`, source: "designer" as const, save_table: tableName || "", save_table_label: tableLabel || tableName, save_column: ref.column, join_table: ref.refTable, join_table_label: ref.refTableLabel, join_column: ref.refColumn, display_column: "", join_type: "LEFT", })); // 2. 필터 테이블들의 조인 설정 (화면 노드에서 열었을 때) const filterTableJoins: UnifiedJoinItem[] = (existingConfig?.filterTables || []).flatMap((ft, ftIdx) => (ft.joinColumnRefs || []).map((ref, refIdx) => ({ id: `designer-filter-${ftIdx}-${refIdx}`, source: "designer" as const, save_table: ft.tableName, save_table_label: ft.tableLabel || ft.tableName, save_column: ref.column, join_table: ref.refTable, join_table_label: ref.refTableLabel, join_column: ref.refColumn, display_column: "", join_type: "LEFT", })) ); // 모든 디자이너 조인 설정 통합 const designerJoins: UnifiedJoinItem[] = [...directJoins, ...filterTableJoins]; // DB 조인 설정을 통합 형식으로 변환 const dbJoins: UnifiedJoinItem[] = fieldJoins.map((item) => ({ id: item.id, source: "db" as const, save_table: item.save_table, save_table_label: item.save_table_label, save_column: item.save_column, join_table: item.join_table, join_table_label: item.join_table_label, join_column: item.join_column, display_column: item.display_column, join_type: item.join_type, })); // 통합된 조인 목록 (화면 디자이너 + DB) const unifiedJoins = [...designerJoins, ...dbJoins]; // 저장 테이블 변경 시 컬럼 로드 useEffect(() => { if (formData.save_table) { onLoadColumns(formData.save_table); } }, [formData.save_table, onLoadColumns]); // 조인 테이블 변경 시 컬럼 로드 useEffect(() => { if (formData.join_table) { onLoadColumns(formData.join_table); } }, [formData.join_table, onLoadColumns]); // 폼 초기화 const resetForm = () => { setFormData({ field_name: "", save_table: tableName || "", save_column: "", join_table: "", join_column: "", display_column: "", join_type: "LEFT", filter_condition: "", is_active: "Y", }); setEditItem(null); setEditingDesignerItem(null); setIsEditing(false); }; // 수정 모드 (DB 설정) const handleEdit = (item: FieldJoin) => { setEditItem(item); setEditingDesignerItem(null); setFormData({ field_name: item.field_name || "", save_table: item.save_table, save_column: item.save_column, join_table: item.join_table, join_column: item.join_column, display_column: item.display_column, join_type: item.join_type, filter_condition: item.filter_condition || "", is_active: item.is_active, }); setIsEditing(true); // 컬럼 로드 onLoadColumns(item.save_table); onLoadColumns(item.join_table); }; // 통합 목록에서 수정 버튼 클릭 const handleEditUnified = (item: UnifiedJoinItem) => { if (item.source === "db") { // DB 설정은 기존 로직 사용 const originalItem = fieldJoins.find(j => j.id === item.id); if (originalItem) handleEdit(originalItem); } else { // 화면 디자이너 설정은 폼에 채우고 새로 저장하도록 setEditItem(null); setEditingDesignerItem(item); setFormData({ field_name: "", save_table: item.save_table, save_column: item.save_column, join_table: item.join_table, join_column: item.join_column, display_column: item.display_column || "", join_type: item.join_type, filter_condition: "", is_active: "Y", }); setIsEditing(true); // 컬럼 로드 onLoadColumns(item.save_table); onLoadColumns(item.join_table); } }; // 통합 목록에서 삭제 버튼 클릭 const handleDeleteUnified = async (item: UnifiedJoinItem) => { if (item.source === "db") { // DB 설정만 삭제 가능 await handleDelete(item.id as number); } else { // 화면 디자이너 설정은 삭제 불가 (화면 디자이너에서 수정해야 함) toast.info("화면 디자이너 설정은 화면 디자이너에서 수정해주세요."); } }; // 저장 const handleSave = async () => { if (!formData.save_table || !formData.save_column || !formData.join_table || !formData.join_column) { toast.error("필수 필드를 모두 입력해주세요."); return; } try { const payload = { screen_id: screenId, ...formData, }; let response; if (editItem) { response = await updateFieldJoin(editItem.id, payload); } else { response = await createFieldJoin(payload); } if (response.success) { toast.success(editItem ? "조인 설정이 수정되었습니다." : "조인 설정이 추가되었습니다."); resetForm(); onReload(); onRefreshVisualization?.(); } else { toast.error(response.message || "저장에 실패했습니다."); } } catch (error: any) { toast.error(error.message || "저장 중 오류가 발생했습니다."); } }; // 삭제 const handleDelete = async (id: number) => { if (!confirm("정말 삭제하시겠습니까?")) return; try { const response = await deleteFieldJoin(id); if (response.success) { toast.success("조인 설정이 삭제되었습니다."); onReload(); onRefreshVisualization?.(); } else { toast.error(response.message || "삭제에 실패했습니다."); } } catch (error: any) { toast.error(error.message || "삭제 중 오류가 발생했습니다."); } }; // 저장 테이블 컬럼 const saveTableColumns = tableColumns[formData.save_table] || []; // 조인 테이블 컬럼 const joinTableColumns = tableColumns[formData.join_table] || []; return (
{/* 필터링 컬럼 정보 */} {existingConfig?.filterColumns && existingConfig.filterColumns.length > 0 && (
필터링 컬럼 (마스터-디테일 연동)
{existingConfig.filterColumns.map((col, idx) => ( {col} ))}

* 이 컬럼들을 기준으로 상위 화면에서 데이터가 필터링됩니다.

)} {/* 참조 정보 (이 테이블을 참조하는 다른 테이블들) */} {existingConfig?.referencedBy && existingConfig.referencedBy.length > 0 && (
이 테이블을 참조하는 관계
참조하는 테이블 참조 유형 연결 {existingConfig.referencedBy.map((ref, idx) => ( {ref.fromTableLabel || ref.fromTable} {ref.relationType} {ref.fromColumn} → {ref.toColumn} ))}
)} {/* 입력 폼 */}
{isEditing ? "조인 설정 수정" : "새 조인 설정 추가"}
{/* 저장 테이블 */}
{ setFormData(prev => ({ ...prev, save_table: v, save_column: "" })); }} options={tables.map((t) => ({ value: t.tableName, label: t.displayName || t.tableName, description: t.tableName !== t.displayName ? t.tableName : undefined, }))} placeholder="테이블 선택" searchPlaceholder="테이블 검색..." />
{/* 저장 컬럼 */}
setFormData(prev => ({ ...prev, save_column: v }))} disabled={!formData.save_table} options={saveTableColumns.map((c) => ({ value: c.columnName, label: c.displayName || c.columnName, description: c.columnName !== c.displayName ? c.columnName : undefined, }))} placeholder="컬럼 선택" searchPlaceholder="컬럼 검색..." />
{/* 조인 타입 */}
setFormData(prev => ({ ...prev, join_type: v }))} options={[ { value: "LEFT", label: "LEFT JOIN" }, { value: "INNER", label: "INNER JOIN" }, { value: "RIGHT", label: "RIGHT JOIN" }, ]} placeholder="조인 타입" searchPlaceholder="타입 검색..." />
{/* 조인 테이블 */}
{ setFormData(prev => ({ ...prev, join_table: v, join_column: "", display_column: "" })); }} options={tables.map((t) => ({ value: t.tableName, label: t.displayName || t.tableName, description: t.tableName !== t.displayName ? t.tableName : undefined, }))} placeholder="테이블 선택" searchPlaceholder="테이블 검색..." />
{/* 조인 컬럼 */}
setFormData(prev => ({ ...prev, join_column: v }))} disabled={!formData.join_table} options={joinTableColumns.map((c) => ({ value: c.columnName, label: c.displayName || c.columnName, description: c.columnName !== c.displayName ? c.columnName : undefined, }))} placeholder="컬럼 선택" searchPlaceholder="컬럼 검색..." />
{/* 표시 컬럼 */}
setFormData(prev => ({ ...prev, display_column: v }))} disabled={!formData.join_table} options={joinTableColumns.map((c) => ({ value: c.columnName, label: c.displayName || c.columnName, description: c.columnName !== c.displayName ? c.columnName : undefined, }))} placeholder="표시할 컬럼 선택" searchPlaceholder="컬럼 검색..." />
{isEditing && ( )}
{/* 통합 조인 목록 */}
조인 설정 목록 총 {unifiedJoins.length}개
출처 저장 테이블 FK 컬럼 조인 테이블 PK 컬럼 타입 작업 {loading ? ( ) : unifiedJoins.length === 0 ? ( 등록된 조인 설정이 없습니다. ) : ( unifiedJoins.map((item) => ( {item.source === "designer" ? ( 화면 ) : ( DB )} {item.save_table_label || item.save_table} {item.save_column} {item.join_table_label || item.join_table} {item.join_column} {item.join_type}
{item.source === "db" && ( )}
)) )}
{designerJoins.length > 0 && (
* 화면: 화면 디자이너 설정 (수정 시 DB에 저장) | * DB: DB 저장 설정 (직접 수정/삭제 가능)
)}
); } // ============================================================ // 탭3: 데이터 흐름 // ============================================================ interface DataFlowTabProps { screenId: number; groupId?: number; groupScreens: Array<{ screen_id: number; screen_name: string }>; dataFlows: DataFlow[]; loading: boolean; onReload: () => void; onRefreshVisualization?: () => void; } function DataFlowTab({ screenId, groupId, groupScreens, dataFlows, loading, onReload, onRefreshVisualization, }: DataFlowTabProps) { const [isEditing, setIsEditing] = useState(false); const [editItem, setEditItem] = useState(null); const [formData, setFormData] = useState({ source_screen_id: screenId, source_action: "", target_screen_id: 0, target_action: "", flow_type: "unidirectional", flow_label: "", is_active: "Y", }); // 폼 초기화 const resetForm = () => { setFormData({ source_screen_id: screenId, source_action: "", target_screen_id: 0, target_action: "", flow_type: "unidirectional", flow_label: "", is_active: "Y", }); setEditItem(null); setIsEditing(false); }; // 수정 모드 const handleEdit = (item: DataFlow) => { setEditItem(item); setFormData({ source_screen_id: item.source_screen_id, source_action: item.source_action || "", target_screen_id: item.target_screen_id, target_action: item.target_action || "", flow_type: item.flow_type, flow_label: item.flow_label || "", is_active: item.is_active, }); setIsEditing(true); }; // 저장 const handleSave = async () => { if (!formData.source_screen_id || !formData.target_screen_id) { toast.error("소스 화면과 타겟 화면을 선택해주세요."); return; } try { const payload = { group_id: groupId, ...formData, }; let response; if (editItem) { response = await updateDataFlow(editItem.id, payload); } else { response = await createDataFlow(payload); } if (response.success) { toast.success(editItem ? "데이터 흐름이 수정되었습니다." : "데이터 흐름이 추가되었습니다."); resetForm(); onReload(); onRefreshVisualization?.(); } else { toast.error(response.message || "저장에 실패했습니다."); } } catch (error: any) { toast.error(error.message || "저장 중 오류가 발생했습니다."); } }; // 삭제 const handleDelete = async (id: number) => { if (!confirm("정말 삭제하시겠습니까?")) return; try { const response = await deleteDataFlow(id); if (response.success) { toast.success("데이터 흐름이 삭제되었습니다."); onReload(); onRefreshVisualization?.(); } else { toast.error(response.message || "삭제에 실패했습니다."); } } catch (error: any) { toast.error(error.message || "삭제 중 오류가 발생했습니다."); } }; // 그룹 없음 안내 if (!groupId) { return (

그룹 정보가 없습니다

데이터 흐름 설정은 화면 그룹 내에서만 사용할 수 있습니다.

); } return (
{/* 입력 폼 */}
{isEditing ? "데이터 흐름 수정" : "새 데이터 흐름 추가"}
{/* 소스 화면 */}
setFormData(prev => ({ ...prev, source_screen_id: parseInt(v) }))} options={groupScreens.map((s) => ({ value: s.screen_id.toString(), label: s.screen_name, }))} placeholder="화면 선택" searchPlaceholder="화면 검색..." />
{/* 소스 액션 */}
setFormData(prev => ({ ...prev, source_action: e.target.value }))} placeholder="예: 행 선택" className="h-9 text-xs" />
{/* 타겟 화면 */}
setFormData(prev => ({ ...prev, target_screen_id: parseInt(v) }))} options={groupScreens .filter(s => s.screen_id !== formData.source_screen_id) .map((s) => ({ value: s.screen_id.toString(), label: s.screen_name, }))} placeholder="화면 선택" searchPlaceholder="화면 검색..." />
{/* 흐름 타입 */}
setFormData(prev => ({ ...prev, flow_type: v }))} options={[ { value: "unidirectional", label: "단방향" }, { value: "bidirectional", label: "양방향" }, ]} placeholder="흐름 타입" searchPlaceholder="타입 검색..." />
{isEditing && ( )}
{/* 목록 */}
소스 화면 액션 타겟 화면 흐름 작업 {loading ? ( ) : dataFlows.length === 0 ? ( 등록된 데이터 흐름이 없습니다. ) : ( dataFlows.map((item) => ( {item.source_screen_name || `화면 ${item.source_screen_id}`} {item.source_action || "-"} {item.target_screen_name || `화면 ${item.target_screen_id}`} {item.flow_type === "bidirectional" ? "양방향" : "단방향"}
)) )}
); } // ============================================================ // 탭4: 필드-컬럼 매핑 (화면 컴포넌트와 DB 컬럼 연결) // ============================================================ interface FieldMappingTabProps { screenId: number; tableName?: string; tableColumns: ColumnTypeInfo[]; loading: boolean; } function FieldMappingTab({ screenId, tableName, tableColumns, loading, }: FieldMappingTabProps) { // 필드 매핑은 screen_layouts.properties에서 관리됨 // 이 탭에서는 현재 매핑 상태를 조회하고 편집 가능하게 제공 return (
필드-컬럼 매핑

화면 컴포넌트와 데이터베이스 컬럼 간의 바인딩을 설정합니다.
현재는 화면 디자이너에서 설정된 내용을 확인할 수 있습니다.

{/* 테이블 컬럼 목록 */} {tableName && (
테이블: {tableName}
컬럼명 한글명 데이터 타입 웹 타입 PK {loading ? ( ) : tableColumns.length === 0 ? ( 컬럼 정보가 없습니다. ) : ( tableColumns.slice(0, 20).map((col) => ( {col.columnName} {col.displayName} {col.dbType} {col.webType} {col.isPrimaryKey && ( PK )} )) )}
{tableColumns.length > 20 && (
+ {tableColumns.length - 20}개 더 있음
)}
)} {!tableName && (

테이블 정보가 없습니다

테이블 노드에서 더블클릭하여 필드 매핑을 확인하세요.

)}
); }