diff --git a/frontend/components/screen/ScreenSettingModal.tsx b/frontend/components/screen/ScreenSettingModal.tsx index b0f85351..52f4ebd5 100644 --- a/frontend/components/screen/ScreenSettingModal.tsx +++ b/frontend/components/screen/ScreenSettingModal.tsx @@ -8,7 +8,6 @@ import { 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"; @@ -240,14 +239,14 @@ export function ScreenSettingModal({ componentCount = 0, onSaveSuccess, }: ScreenSettingModalProps) { - const [activeTab, setActiveTab] = useState("overview"); const [loading, setLoading] = useState(false); const [dataFlows, setDataFlows] = useState([]); const [layoutItems, setLayoutItems] = useState([]); - const [iframeKey, setIframeKey] = useState(0); // iframe 새로고침용 키 - const [canvasSize, setCanvasSize] = useState({ width: 0, height: 0 }); // 화면 캔버스 크기 - const [showDesignerModal, setShowDesignerModal] = useState(false); // 화면 디자이너 모달 - const [showTableSettingModal, setShowTableSettingModal] = useState(false); // 테이블 설정 모달 + const [buttonControls, setButtonControls] = useState([]); + const [iframeKey, setIframeKey] = useState(0); + const [canvasSize, setCanvasSize] = useState({ width: 0, height: 0 }); + const [showDesignerModal, setShowDesignerModal] = useState(false); + const [showTableSettingModal, setShowTableSettingModal] = useState(false); const [tableSettingTarget, setTableSettingTarget] = useState<{ tableName: string; tableLabel?: string } | null>(null); // 그룹 내 화면 목록 및 현재 선택된 화면 @@ -338,12 +337,56 @@ export function ScreenSettingModal({ if (layoutResponse.success && layoutResponse.data) { const screenLayout = layoutResponse.data[currentScreenId]; setLayoutItems(screenLayout?.layoutItems || []); - // 캔버스 크기 저장 (화면 프리뷰에 사용) setCanvasSize({ width: screenLayout?.canvasWidth || 0, height: screenLayout?.canvasHeight || 0, }); } + + // 3. 버튼 정보 추출 (읽기 전용 요약용) + try { + const rawLayout = await screenApi.getLayout(currentScreenId); + if (rawLayout?.components) { + const buttons: ButtonControlInfo[] = []; + const extractButtons = (components: any[]) => { + for (const comp of components) { + const config = comp.componentConfig || {}; + const isButton = + comp.widgetType === "button" || comp.webType === "button" || + comp.type === "button" || config.webType === "button" || + comp.componentType?.includes("button") || comp.componentKind?.includes("button"); + if (isButton) { + const webTypeConfig = comp.webTypeConfig || {}; + const action = config.action || {}; + buttons.push({ + id: comp.id || comp.componentId || `btn-${buttons.length}`, + label: config.text || comp.label || comp.title || comp.name || "버튼", + actionType: typeof action === "string" ? action : (action.type || "custom"), + confirmMessage: action.confirmationMessage || action.confirmMessage || config.confirmMessage, + confirmationEnabled: action.confirmationEnabled ?? (!!action.confirmationMessage || !!action.confirmMessage), + backgroundColor: webTypeConfig.backgroundColor || config.backgroundColor || comp.style?.backgroundColor, + textColor: webTypeConfig.textColor || config.textColor || comp.style?.color, + borderRadius: webTypeConfig.borderRadius || config.borderRadius || comp.style?.borderRadius, + linkedFlows: webTypeConfig.dataflowConfig?.flowConfigs?.map((fc: any) => ({ + id: fc.flowId, name: fc.flowName, timing: fc.executionTiming || "after", + })) || (webTypeConfig.dataflowConfig?.flowConfig ? [{ + id: webTypeConfig.dataflowConfig.flowConfig.flowId, + name: webTypeConfig.dataflowConfig.flowConfig.flowName, + timing: webTypeConfig.dataflowConfig.flowConfig.executionTiming || "after", + }] : []), + }); + } + if (comp.children && Array.isArray(comp.children)) extractButtons(comp.children); + if (comp.componentConfig?.children && Array.isArray(comp.componentConfig.children)) extractButtons(comp.componentConfig.children); + if (comp.items && Array.isArray(comp.items)) extractButtons(comp.items); + } + }; + extractButtons(rawLayout.components); + setButtonControls(buttons); + } + } catch (btnError) { + console.error("버튼 정보 추출 실패:", btnError); + } } catch (error) { console.error("데이터 로드 실패:", error); } finally { @@ -360,162 +403,295 @@ export function ScreenSettingModal({ // 새로고침 (데이터 + iframe) const handleRefresh = useCallback(() => { loadData(); - setIframeKey(prev => prev + 1); // iframe 새로고침 + setIframeKey(prev => prev + 1); }, [loadData]); + // 통계 계산 + const stats = useMemo(() => { + const totalJoins = filterTables.reduce((sum, ft) => sum + (ft.joinColumnRefs?.length || 0), 0); + const layoutColumnsSet = new Set(); + layoutItems.forEach((item) => { + if (item.usedColumns) item.usedColumns.forEach((col) => layoutColumnsSet.add(col)); + if (item.bindField) layoutColumnsSet.add(item.bindField); + }); + const inputCount = layoutItems.filter(i => !i.widgetType?.includes("button") && !i.componentKind?.includes("table")).length; + const gridCount = layoutItems.filter(i => i.componentKind?.includes("table") || i.componentKind?.includes("grid")).length; + return { + tableCount: 1 + filterTables.length, + fieldCount: layoutColumnsSet.size || fieldMappings.length, + joinCount: totalJoins, + flowCount: dataFlows.length, + inputCount, + gridCount, + buttonCount: buttonControls.length, + }; + }, [filterTables, fieldMappings, dataFlows, layoutItems, buttonControls]); + + // 연결된 플로우 총 개수 + const linkedFlowCount = useMemo(() => { + return buttonControls.reduce((sum, btn) => sum + (btn.linkedFlows?.length || 0), 0); + }, [buttonControls]); + return ( <> - - - - - 화면 설정: - {groupScreens.length > 1 ? ( - - ) : ( - {currentScreenName} + + {/* V3 Header */} + + + + {currentScreenName} + {groupScreens.length > 1 && ( + <> + + + )} + #{currentScreenId} + - - 화면의 필드 매핑, 테이블 연결, 데이터 흐름을 확인하고 설정합니다. - + 화면 정보 패널 - {/* 2컬럼 레이아웃: 왼쪽 탭(좁게) + 오른쪽 프리뷰(넓게) */} -
- {/* 왼쪽: 탭 컨텐츠 (40%) */} -
- -
- - - - 개요 - - - - 테이블 설정 - - - - 제어 관리 - - - - 데이터 흐름 - - -
- - + {/* V3 Body: Left Info Panel + Right Preview */} +
+ {/* 왼쪽: 정보 패널 (탭 없음, 단일 스크롤) */} +
+
+ + {/* 1. 내러티브 요약 */} +
+

+ {currentMainTable || "테이블 미연결"} + {stats.fieldCount > 0 && <> 테이블의 {stats.fieldCount}개 컬럼을 사용하고 있어요.} + {filterTables.length > 0 && <>
필터 테이블 {filterTables.length}개{stats.joinCount > 0 && <>, 엔티티 조인 {stats.joinCount}개}가 연결되어 있어요.} +

+
+ + {/* 2. 속성 테이블 */} +
+
+ 메인 테이블 + {currentMainTable || "-"} + {stats.fieldCount > 0 && {stats.fieldCount} 컬럼} +
+ {filterTables.map((ft, idx) => ( +
+ 필터 테이블 + {ft.tableName} + FK +
+ ))} + {filterTables.some(ft => ft.joinColumnRefs && ft.joinColumnRefs.length > 0) && ( +
+ 엔티티 조인 + + {filterTables.flatMap(ft => ft.joinColumnRefs || []).map((j, i) => ( + {i > 0 && ", "}{j.column}{j.refTable} + ))} + + {stats.joinCount}개 +
+ )} +
+ 컴포넌트 + + {stats.inputCount > 0 && <>입력 {stats.inputCount}} + {stats.gridCount > 0 && <>{stats.inputCount > 0 && " · "}그리드 {stats.gridCount}} + {stats.buttonCount > 0 && <>{(stats.inputCount > 0 || stats.gridCount > 0) && " · "}버튼 {stats.buttonCount}} + {stats.inputCount === 0 && stats.gridCount === 0 && stats.buttonCount === 0 && `${componentCount}개`} +
- {/* 탭 1: 화면 개요 */} - - - +
- {/* 탭 2: 테이블 설정 */} - - {mainTable && ( - {}} // 탭에서는 닫기 불필요 - tableName={mainTable} - tableLabel={mainTableLabel} - screenId={currentScreenId} - onSaveSuccess={handleRefresh} - isEmbedded={true} // 임베드 모드 - /> + {/* 3. 테이블 섹션 */} +
+
+
+ +
+ 테이블 + {stats.tableCount} +
+

컬럼 타입이나 조인을 변경하려면 "설정"을 눌러요

+
+ {currentMainTable && ( +
+
+
+
{currentMainTable}
+
메인 · {stats.fieldCount} 컬럼 사용중
+
+ +
+ )} + {filterTables.map((ft, idx) => ( +
+
+
+
{ft.tableName}
+
필터{ft.filterKeyMapping ? ` · FK: ${ft.filterKeyMapping.filterTableColumn}` : ""}
+
+ +
+ ))} +
+
+ +
+ + {/* 4. 버튼 섹션 (읽기 전용) */} +
+
+
+ +
+ 버튼 + {stats.buttonCount} +
+

버튼 편집은 화면 디자이너에서 해요

+ {buttonControls.length > 0 ? ( +
+ {buttonControls.map((btn) => ( +
+ {btn.label} +
+
{btn.actionType?.toUpperCase() || "CUSTOM"}
+ {btn.confirmMessage &&
"{btn.confirmMessage}"
} +
+ {btn.linkedFlows && btn.linkedFlows.length > 0 && ( + + 플로우 {btn.linkedFlows.length} + + )} +
+ ))} +
+ ) : ( +
버튼이 없어요
)} - +
- {/* 탭 3: 제어 관리 */} - - - +
- {/* 탭 3: 데이터 흐름 */} - - - - + {/* 5. 데이터 흐름 섹션 */} +
+
+
+ +
+ 데이터 흐름 + {stats.flowCount} +
+ {dataFlows.length > 0 ? ( +
+ {dataFlows.map((flow) => ( +
+
+
+
{flow.source_action || flow.flow_type} → {flow.target_screen_name || `화면 ${flow.target_screen_id}`}
+
{flow.flow_type}{flow.flow_label ? ` · ${flow.flow_label}` : ""}
+
+ +
+ ))} +
+ ) : ( +
+ +
데이터 흐름이 없어요
+
다른 화면으로 데이터를 전달하려면 추가해보세요
+
+ )} + +
+ +
+ + {/* 6. 플로우 연동 섹션 */} +
+
+
+ +
+ 플로우 연동 + {linkedFlowCount} +
+ {linkedFlowCount > 0 ? ( +
+ {buttonControls.filter(b => b.linkedFlows && b.linkedFlows.length > 0).flatMap(btn => + (btn.linkedFlows || []).map(flow => ( +
+
+
+
{flow.name || `플로우 #${flow.id}`}
+
{btn.label} 버튼 · {flow.timing === "before" ? "실행 전" : "실행 후"}
+
+
+ )) + )} +
+ ) : ( +
연동된 플로우가 없어요
+ )} +
+
+ + {/* CTA: 화면 디자이너 열기 */} +
+ +
- {/* 오른쪽: 화면 프리뷰 (60%, 항상 표시) */} -
- + + {/* 테이블 선택 드롭다운 (여러 테이블 + showTableSelector 활성 시) */} + {showTableSelector && hasMultipleTables && ( + + )} + {/* 필터 입력 필드들 */} {activeFilters.length > 0 && (