This commit is contained in:
parent
6a50e1e924
commit
4ba931dc70
|
|
@ -8,7 +8,6 @@ import {
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
|
|
@ -240,14 +239,14 @@ export function ScreenSettingModal({
|
||||||
componentCount = 0,
|
componentCount = 0,
|
||||||
onSaveSuccess,
|
onSaveSuccess,
|
||||||
}: ScreenSettingModalProps) {
|
}: ScreenSettingModalProps) {
|
||||||
const [activeTab, setActiveTab] = useState("overview");
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [dataFlows, setDataFlows] = useState<DataFlow[]>([]);
|
const [dataFlows, setDataFlows] = useState<DataFlow[]>([]);
|
||||||
const [layoutItems, setLayoutItems] = useState<LayoutItem[]>([]);
|
const [layoutItems, setLayoutItems] = useState<LayoutItem[]>([]);
|
||||||
const [iframeKey, setIframeKey] = useState(0); // iframe 새로고침용 키
|
const [buttonControls, setButtonControls] = useState<ButtonControlInfo[]>([]);
|
||||||
const [canvasSize, setCanvasSize] = useState({ width: 0, height: 0 }); // 화면 캔버스 크기
|
const [iframeKey, setIframeKey] = useState(0);
|
||||||
const [showDesignerModal, setShowDesignerModal] = useState(false); // 화면 디자이너 모달
|
const [canvasSize, setCanvasSize] = useState({ width: 0, height: 0 });
|
||||||
const [showTableSettingModal, setShowTableSettingModal] = useState(false); // 테이블 설정 모달
|
const [showDesignerModal, setShowDesignerModal] = useState(false);
|
||||||
|
const [showTableSettingModal, setShowTableSettingModal] = useState(false);
|
||||||
const [tableSettingTarget, setTableSettingTarget] = useState<{ tableName: string; tableLabel?: string } | null>(null);
|
const [tableSettingTarget, setTableSettingTarget] = useState<{ tableName: string; tableLabel?: string } | null>(null);
|
||||||
|
|
||||||
// 그룹 내 화면 목록 및 현재 선택된 화면
|
// 그룹 내 화면 목록 및 현재 선택된 화면
|
||||||
|
|
@ -338,12 +337,56 @@ export function ScreenSettingModal({
|
||||||
if (layoutResponse.success && layoutResponse.data) {
|
if (layoutResponse.success && layoutResponse.data) {
|
||||||
const screenLayout = layoutResponse.data[currentScreenId];
|
const screenLayout = layoutResponse.data[currentScreenId];
|
||||||
setLayoutItems(screenLayout?.layoutItems || []);
|
setLayoutItems(screenLayout?.layoutItems || []);
|
||||||
// 캔버스 크기 저장 (화면 프리뷰에 사용)
|
|
||||||
setCanvasSize({
|
setCanvasSize({
|
||||||
width: screenLayout?.canvasWidth || 0,
|
width: screenLayout?.canvasWidth || 0,
|
||||||
height: screenLayout?.canvasHeight || 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) {
|
} catch (error) {
|
||||||
console.error("데이터 로드 실패:", error);
|
console.error("데이터 로드 실패:", error);
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -360,23 +403,52 @@ export function ScreenSettingModal({
|
||||||
// 새로고침 (데이터 + iframe)
|
// 새로고침 (데이터 + iframe)
|
||||||
const handleRefresh = useCallback(() => {
|
const handleRefresh = useCallback(() => {
|
||||||
loadData();
|
loadData();
|
||||||
setIframeKey(prev => prev + 1); // iframe 새로고침
|
setIframeKey(prev => prev + 1);
|
||||||
}, [loadData]);
|
}, [loadData]);
|
||||||
|
|
||||||
|
// 통계 계산
|
||||||
|
const stats = useMemo(() => {
|
||||||
|
const totalJoins = filterTables.reduce((sum, ft) => sum + (ft.joinColumnRefs?.length || 0), 0);
|
||||||
|
const layoutColumnsSet = new Set<string>();
|
||||||
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<Dialog open={isOpen && !showDesignerModal} onOpenChange={onClose}>
|
<Dialog open={isOpen && !showDesignerModal} onOpenChange={onClose}>
|
||||||
<DialogContent className="flex h-[90vh] max-h-[950px] w-[98vw] max-w-[1600px] flex-col">
|
<DialogContent className="flex h-[90vh] max-h-[950px] w-[98vw] max-w-[1600px] flex-col overflow-hidden border-border/40 bg-background/95 backdrop-blur-xl">
|
||||||
<DialogHeader className="flex-shrink-0">
|
{/* V3 Header */}
|
||||||
<DialogTitle className="flex items-center gap-2 text-lg">
|
<DialogHeader className="flex-shrink-0 pb-0">
|
||||||
<Settings2 className="h-5 w-5 text-primary" />
|
<DialogTitle className="flex items-center gap-3 text-base">
|
||||||
화면 설정:
|
<span className="h-2 w-2 rounded-full bg-emerald-500 shadow-[0_0_6px_rgba(34,197,94,0.4)]" />
|
||||||
{groupScreens.length > 1 ? (
|
<span className="font-bold tracking-tight">{currentScreenName}</span>
|
||||||
|
{groupScreens.length > 1 && (
|
||||||
|
<>
|
||||||
|
<span className="h-3.5 w-px bg-border" />
|
||||||
<Select
|
<Select
|
||||||
value={currentScreenId.toString()}
|
value={currentScreenId.toString()}
|
||||||
onValueChange={(value) => handleScreenChange(parseInt(value, 10))}
|
onValueChange={(value) => handleScreenChange(parseInt(value, 10))}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="h-8 w-auto min-w-[200px] max-w-[400px] text-base font-semibold">
|
<SelectTrigger className="h-7 w-auto min-w-[140px] max-w-[280px] border-border/40 bg-muted/30 text-xs font-medium">
|
||||||
<SelectValue placeholder="화면 선택" />
|
<SelectValue placeholder="화면 선택" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
|
|
@ -384,134 +456,238 @@ export function ScreenSettingModal({
|
||||||
<SelectItem key={screen.screen_id} value={screen.screen_id.toString()}>
|
<SelectItem key={screen.screen_id} value={screen.screen_id.toString()}>
|
||||||
{screen.screen_name}
|
{screen.screen_name}
|
||||||
{screen.screen_role && (
|
{screen.screen_role && (
|
||||||
<span className="ml-2 text-xs text-muted-foreground">
|
<span className="ml-1 text-muted-foreground">({screen.screen_role})</span>
|
||||||
({screen.screen_role})
|
|
||||||
</span>
|
|
||||||
)}
|
)}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
) : (
|
</>
|
||||||
<span>{currentScreenName}</span>
|
|
||||||
)}
|
)}
|
||||||
</DialogTitle>
|
<span className="ml-auto font-mono text-[10px] text-muted-foreground/60">#{currentScreenId}</span>
|
||||||
<DialogDescription className="text-sm">
|
<Button variant="ghost" size="sm" onClick={handleRefresh} className="h-7 w-7 p-0" title="새로고침">
|
||||||
화면의 필드 매핑, 테이블 연결, 데이터 흐름을 확인하고 설정합니다.
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
|
|
||||||
{/* 2컬럼 레이아웃: 왼쪽 탭(좁게) + 오른쪽 프리뷰(넓게) */}
|
|
||||||
<div className="flex min-h-0 flex-1 gap-3">
|
|
||||||
{/* 왼쪽: 탭 컨텐츠 (40%) */}
|
|
||||||
<div className="flex min-h-0 w-[40%] flex-col rounded-lg border bg-card">
|
|
||||||
<Tabs
|
|
||||||
value={activeTab}
|
|
||||||
onValueChange={setActiveTab}
|
|
||||||
className="flex min-h-0 flex-1 flex-col"
|
|
||||||
>
|
|
||||||
<div className="flex flex-shrink-0 items-center justify-between border-b p-2">
|
|
||||||
<TabsList className="h-8">
|
|
||||||
<TabsTrigger value="overview" className="gap-1 text-xs px-2">
|
|
||||||
<Database className="h-3 w-3" />
|
|
||||||
개요
|
|
||||||
</TabsTrigger>
|
|
||||||
<TabsTrigger value="table-setting" className="gap-1 text-xs px-2" disabled={!mainTable}>
|
|
||||||
<Settings2 className="h-3 w-3" />
|
|
||||||
테이블 설정
|
|
||||||
</TabsTrigger>
|
|
||||||
<TabsTrigger value="control-management" className="gap-1 text-xs px-2">
|
|
||||||
<Zap className="h-3 w-3" />
|
|
||||||
제어 관리
|
|
||||||
</TabsTrigger>
|
|
||||||
<TabsTrigger value="data-flow" className="gap-1 text-xs px-2">
|
|
||||||
<GitBranch className="h-3 w-3" />
|
|
||||||
데이터 흐름
|
|
||||||
</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={handleRefresh}
|
|
||||||
className="h-7 w-7 p-0"
|
|
||||||
title="새로고침"
|
|
||||||
>
|
|
||||||
<RefreshCw className={cn("h-3.5 w-3.5", loading && "animate-spin")} />
|
<RefreshCw className={cn("h-3.5 w-3.5", loading && "animate-spin")} />
|
||||||
</Button>
|
</Button>
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription className="sr-only">화면 정보 패널</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
{/* V3 Body: Left Info Panel + Right Preview */}
|
||||||
|
<div className="flex min-h-0 flex-1 gap-3 pt-2">
|
||||||
|
{/* 왼쪽: 정보 패널 (탭 없음, 단일 스크롤) */}
|
||||||
|
<div className="flex min-h-0 w-[380px] flex-shrink-0 flex-col rounded-lg border border-border/40">
|
||||||
|
<div className="flex-1 overflow-y-auto p-4 [&::-webkit-scrollbar]:w-[2px] [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-border/50">
|
||||||
|
|
||||||
|
{/* 1. 내러티브 요약 */}
|
||||||
|
<div className="mb-4 rounded-lg border border-primary/10 bg-gradient-to-br from-primary/[0.04] to-blue-500/[0.02] p-3">
|
||||||
|
<p className="text-xs leading-relaxed text-muted-foreground">
|
||||||
|
<span className="font-semibold text-foreground">{currentMainTable || "테이블 미연결"}</span>
|
||||||
|
{stats.fieldCount > 0 && <> 테이블의 <span className="font-bold text-primary">{stats.fieldCount}개</span> 컬럼을 사용하고 있어요.</>}
|
||||||
|
{filterTables.length > 0 && <><br />필터 테이블 {filterTables.length}개{stats.joinCount > 0 && <>, 엔티티 조인 {stats.joinCount}개</>}가 연결되어 있어요.</>}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 2. 속성 테이블 */}
|
||||||
|
<div className="mb-4 space-y-0.5">
|
||||||
|
<div className="flex h-[30px] items-center rounded-md px-2 text-[11px] transition-colors hover:bg-muted/30">
|
||||||
|
<span className="w-[76px] flex-shrink-0 text-[10px] font-medium text-muted-foreground">메인 테이블</span>
|
||||||
|
<span className="min-w-0 flex-1 truncate font-mono text-[10px] font-semibold">{currentMainTable || "-"}</span>
|
||||||
|
{stats.fieldCount > 0 && <span className="flex-shrink-0 font-mono text-[9px] text-muted-foreground">{stats.fieldCount} 컬럼</span>}
|
||||||
|
</div>
|
||||||
|
{filterTables.map((ft, idx) => (
|
||||||
|
<div key={idx} className="flex h-[30px] items-center rounded-md px-2 text-[11px] transition-colors hover:bg-muted/30">
|
||||||
|
<span className="w-[76px] flex-shrink-0 text-[10px] font-medium text-muted-foreground">필터 테이블</span>
|
||||||
|
<span className="min-w-0 flex-1 truncate font-mono text-[10px] font-semibold text-emerald-500">{ft.tableName}</span>
|
||||||
|
<span className="flex-shrink-0 font-mono text-[9px] text-muted-foreground">FK</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{filterTables.some(ft => ft.joinColumnRefs && ft.joinColumnRefs.length > 0) && (
|
||||||
|
<div className="flex h-[30px] items-center rounded-md px-2 text-[11px] transition-colors hover:bg-muted/30">
|
||||||
|
<span className="w-[76px] flex-shrink-0 text-[10px] font-medium text-muted-foreground">엔티티 조인</span>
|
||||||
|
<span className="min-w-0 flex-1 truncate text-[10px] font-semibold">
|
||||||
|
{filterTables.flatMap(ft => ft.joinColumnRefs || []).map((j, i) => (
|
||||||
|
<span key={i}>{i > 0 && ", "}<span className="font-mono">{j.column}</span> → <span className="font-mono text-amber-500">{j.refTable}</span></span>
|
||||||
|
))}
|
||||||
|
</span>
|
||||||
|
<span className="flex-shrink-0 font-mono text-[9px] text-muted-foreground">{stats.joinCount}개</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex h-[30px] items-center rounded-md px-2 text-[11px] transition-colors hover:bg-muted/30">
|
||||||
|
<span className="w-[76px] flex-shrink-0 text-[10px] font-medium text-muted-foreground">컴포넌트</span>
|
||||||
|
<span className="min-w-0 flex-1 truncate text-[10px] font-semibold">
|
||||||
|
{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}개`}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="my-4 h-px bg-border/40" />
|
||||||
|
|
||||||
|
{/* 3. 테이블 섹션 */}
|
||||||
|
<div className="mb-4">
|
||||||
|
<div className="mb-2 flex items-center gap-1.5 border-b border-border/30 pb-2">
|
||||||
|
<div className="flex h-4 w-4 items-center justify-center rounded bg-blue-500/10">
|
||||||
|
<Database className="h-2.5 w-2.5 text-blue-500" />
|
||||||
|
</div>
|
||||||
|
<span className="text-[11px] font-bold uppercase tracking-wide text-muted-foreground">테이블</span>
|
||||||
|
<Badge variant="secondary" className="ml-auto h-4 rounded-full px-1.5 text-[9px] font-bold bg-blue-500/10 text-blue-500 hover:bg-blue-500/10">{stats.tableCount}</Badge>
|
||||||
|
</div>
|
||||||
|
<p className="mb-2 text-[10px] text-muted-foreground/70">컬럼 타입이나 조인을 변경하려면 "설정"을 눌러요</p>
|
||||||
|
<div className="space-y-1">
|
||||||
|
{currentMainTable && (
|
||||||
|
<div className="flex items-center gap-2.5 rounded-lg border border-border/40 bg-muted/20 p-2.5 transition-colors hover:border-border/60 hover:bg-muted/30">
|
||||||
|
<div className="h-6 w-[3px] flex-shrink-0 rounded-full bg-blue-500" />
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<div className="truncate font-mono text-[11px] font-semibold">{currentMainTable}</div>
|
||||||
|
<div className="text-[9px] text-muted-foreground">메인 · {stats.fieldCount} 컬럼 사용중</div>
|
||||||
|
</div>
|
||||||
|
<Button variant="outline" size="sm" className="h-6 px-2.5 text-[10px] font-semibold border-border/50" onClick={() => handleOpenTableSetting(currentMainTable, currentMainTableLabel)}>설정</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{filterTables.map((ft, idx) => (
|
||||||
|
<div key={idx} className="flex items-center gap-2.5 rounded-lg border border-border/40 bg-muted/20 p-2.5 transition-colors hover:border-border/60 hover:bg-muted/30">
|
||||||
|
<div className="h-6 w-[3px] flex-shrink-0 rounded-full bg-emerald-500" />
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<div className="truncate font-mono text-[11px] font-semibold">{ft.tableName}</div>
|
||||||
|
<div className="text-[9px] text-muted-foreground">필터{ft.filterKeyMapping ? ` · FK: ${ft.filterKeyMapping.filterTableColumn}` : ""}</div>
|
||||||
|
</div>
|
||||||
|
<Button variant="outline" size="sm" className="h-6 px-2.5 text-[10px] font-semibold border-border/50" onClick={() => handleOpenTableSetting(ft.tableName, ft.tableLabel)}>설정</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="my-4 h-px bg-border/40" />
|
||||||
|
|
||||||
|
{/* 4. 버튼 섹션 (읽기 전용) */}
|
||||||
|
<div className="mb-4">
|
||||||
|
<div className="mb-2 flex items-center gap-1.5 border-b border-border/30 pb-2">
|
||||||
|
<div className="flex h-4 w-4 items-center justify-center rounded bg-amber-500/10">
|
||||||
|
<MousePointer className="h-2.5 w-2.5 text-amber-500" />
|
||||||
|
</div>
|
||||||
|
<span className="text-[11px] font-bold uppercase tracking-wide text-muted-foreground">버튼</span>
|
||||||
|
<Badge variant="secondary" className="ml-auto h-4 rounded-full px-1.5 text-[9px] font-bold bg-amber-500/10 text-amber-500 hover:bg-amber-500/10">{stats.buttonCount}</Badge>
|
||||||
|
</div>
|
||||||
|
<p className="mb-2 text-[10px] text-muted-foreground/70">버튼 편집은 화면 디자이너에서 해요</p>
|
||||||
|
{buttonControls.length > 0 ? (
|
||||||
|
<div className="space-y-0">
|
||||||
|
{buttonControls.map((btn) => (
|
||||||
|
<div key={btn.id} className="flex items-center gap-2 border-b border-border/20 py-1.5 last:border-b-0">
|
||||||
|
<span
|
||||||
|
className="flex h-5 flex-shrink-0 items-center rounded px-2 text-[9px] font-bold"
|
||||||
|
style={{
|
||||||
|
backgroundColor: btn.backgroundColor ? `${btn.backgroundColor}20` : "hsl(var(--muted))",
|
||||||
|
color: btn.textColor || btn.backgroundColor || "hsl(var(--foreground))",
|
||||||
|
}}
|
||||||
|
>{btn.label}</span>
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<div className="text-[10px] text-muted-foreground">{btn.actionType?.toUpperCase() || "CUSTOM"}</div>
|
||||||
|
{btn.confirmMessage && <div className="truncate text-[9px] italic text-muted-foreground/60">"{btn.confirmMessage}"</div>}
|
||||||
|
</div>
|
||||||
|
{btn.linkedFlows && btn.linkedFlows.length > 0 && (
|
||||||
|
<Badge variant="secondary" className="h-4 rounded px-1.5 text-[8px] font-bold bg-violet-500/10 text-violet-500 hover:bg-violet-500/10">
|
||||||
|
플로우 {btn.linkedFlows.length}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="py-4 text-center text-[10px] text-muted-foreground/50">버튼이 없어요</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="my-4 h-px bg-border/40" />
|
||||||
|
|
||||||
|
{/* 5. 데이터 흐름 섹션 */}
|
||||||
|
<div className="mb-4">
|
||||||
|
<div className="mb-2 flex items-center gap-1.5 border-b border-border/30 pb-2">
|
||||||
|
<div className="flex h-4 w-4 items-center justify-center rounded bg-rose-500/10">
|
||||||
|
<ArrowRight className="h-2.5 w-2.5 text-rose-500" />
|
||||||
|
</div>
|
||||||
|
<span className="text-[11px] font-bold uppercase tracking-wide text-muted-foreground">데이터 흐름</span>
|
||||||
|
<Badge variant="secondary" className="ml-auto h-4 rounded-full px-1.5 text-[9px] font-bold bg-rose-500/10 text-rose-500 hover:bg-rose-500/10">{stats.flowCount}</Badge>
|
||||||
|
</div>
|
||||||
|
{dataFlows.length > 0 ? (
|
||||||
|
<div className="space-y-1">
|
||||||
|
{dataFlows.map((flow) => (
|
||||||
|
<div key={flow.id} className="flex items-center gap-2.5 rounded-lg border border-border/40 bg-muted/20 p-2.5">
|
||||||
|
<div className="h-6 w-[3px] flex-shrink-0 rounded-full bg-rose-500" />
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<div className="truncate text-[11px] font-semibold">{flow.source_action || flow.flow_type} → {flow.target_screen_name || `화면 ${flow.target_screen_id}`}</div>
|
||||||
|
<div className="text-[9px] text-muted-foreground">{flow.flow_type}{flow.flow_label ? ` · ${flow.flow_label}` : ""}</div>
|
||||||
|
</div>
|
||||||
|
<Button variant="ghost" size="sm" className="h-5 w-5 p-0 text-muted-foreground hover:text-destructive" onClick={async () => {
|
||||||
|
if (!confirm("정말 삭제하시겠습니까?")) return;
|
||||||
|
const res = await deleteDataFlow(flow.id);
|
||||||
|
if (res.success) { toast.success("삭제되었습니다."); loadData(); onSaveSuccess?.(); }
|
||||||
|
else toast.error("삭제 실패");
|
||||||
|
}}>
|
||||||
|
<Trash2 className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="rounded-lg border border-dashed border-border/40 py-5 text-center">
|
||||||
|
<ArrowRight className="mx-auto mb-1 h-4 w-4 text-muted-foreground/30" />
|
||||||
|
<div className="text-[11px] font-medium text-muted-foreground/60">데이터 흐름이 없어요</div>
|
||||||
|
<div className="text-[9px] text-muted-foreground/40">다른 화면으로 데이터를 전달하려면 추가해보세요</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<Button variant="outline" size="sm" className="mt-2 h-7 w-full text-[10px] font-semibold border-border/40 text-muted-foreground" onClick={() => toast.info("데이터 흐름 추가는 준비 중이에요")}>
|
||||||
|
<Plus className="mr-1 h-3 w-3" />
|
||||||
|
흐름 추가
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="my-4 h-px bg-border/40" />
|
||||||
|
|
||||||
|
{/* 6. 플로우 연동 섹션 */}
|
||||||
|
<div>
|
||||||
|
<div className="mb-2 flex items-center gap-1.5 border-b border-border/30 pb-2">
|
||||||
|
<div className="flex h-4 w-4 items-center justify-center rounded bg-violet-500/10">
|
||||||
|
<Link2 className="h-2.5 w-2.5 text-violet-500" />
|
||||||
|
</div>
|
||||||
|
<span className="text-[11px] font-bold uppercase tracking-wide text-muted-foreground">플로우 연동</span>
|
||||||
|
<Badge variant="secondary" className="ml-auto h-4 rounded-full px-1.5 text-[9px] font-bold bg-violet-500/10 text-violet-500 hover:bg-violet-500/10">{linkedFlowCount}</Badge>
|
||||||
|
</div>
|
||||||
|
{linkedFlowCount > 0 ? (
|
||||||
|
<div className="space-y-1">
|
||||||
|
{buttonControls.filter(b => b.linkedFlows && b.linkedFlows.length > 0).flatMap(btn =>
|
||||||
|
(btn.linkedFlows || []).map(flow => (
|
||||||
|
<div key={`${btn.id}-${flow.id}`} className="flex items-center gap-2.5 rounded-lg border border-violet-500/10 bg-violet-500/[0.03] p-2.5">
|
||||||
|
<div className="h-6 w-[3px] flex-shrink-0 rounded-full bg-violet-500" />
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<div className="truncate text-[11px] font-semibold">{flow.name || `플로우 #${flow.id}`}</div>
|
||||||
|
<div className="text-[9px] text-muted-foreground">{btn.label} 버튼 · {flow.timing === "before" ? "실행 전" : "실행 후"}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="py-3 text-center text-[10px] text-muted-foreground/50">연동된 플로우가 없어요</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* CTA: 화면 디자이너 열기 */}
|
||||||
|
<div className="flex-shrink-0 border-t border-border/40 p-3">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
className="h-8 w-full gap-1.5 bg-gradient-to-r from-primary to-blue-500 text-[11px] font-bold tracking-tight text-primary-foreground shadow-md shadow-primary/20 transition-all hover:shadow-lg hover:shadow-primary/30"
|
||||||
size="sm"
|
|
||||||
onClick={() => setShowDesignerModal(true)}
|
onClick={() => setShowDesignerModal(true)}
|
||||||
className="h-7 px-2 text-xs gap-1"
|
|
||||||
title="화면 디자이너에서 상세 편집"
|
|
||||||
>
|
>
|
||||||
<ExternalLink className="h-3 w-3" />
|
<ExternalLink className="h-3 w-3" />
|
||||||
디자이너
|
화면 디자이너에서 편집
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 탭 1: 화면 개요 */}
|
{/* 오른쪽: 화면 프리뷰 */}
|
||||||
<TabsContent value="overview" className="mt-0 min-h-0 flex-1 overflow-auto p-3">
|
<div className="flex min-h-0 flex-1 flex-col overflow-hidden rounded-lg border border-border/40">
|
||||||
<OverviewTab
|
|
||||||
screenId={currentScreenId}
|
|
||||||
screenName={currentScreenName}
|
|
||||||
mainTable={currentMainTable}
|
|
||||||
mainTableLabel={currentMainTableLabel}
|
|
||||||
filterTables={filterTables}
|
|
||||||
fieldMappings={fieldMappings}
|
|
||||||
componentCount={componentCount}
|
|
||||||
dataFlows={dataFlows}
|
|
||||||
layoutItems={layoutItems}
|
|
||||||
loading={loading}
|
|
||||||
onRefresh={handleRefresh}
|
|
||||||
onOpenTableSetting={handleOpenTableSetting}
|
|
||||||
/>
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
{/* 탭 2: 테이블 설정 */}
|
|
||||||
<TabsContent value="table-setting" className="mt-0 min-h-0 flex-1 overflow-hidden p-0">
|
|
||||||
{mainTable && (
|
|
||||||
<TableSettingModal
|
|
||||||
isOpen={true}
|
|
||||||
onClose={() => {}} // 탭에서는 닫기 불필요
|
|
||||||
tableName={mainTable}
|
|
||||||
tableLabel={mainTableLabel}
|
|
||||||
screenId={currentScreenId}
|
|
||||||
onSaveSuccess={handleRefresh}
|
|
||||||
isEmbedded={true} // 임베드 모드
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
{/* 탭 3: 제어 관리 */}
|
|
||||||
<TabsContent value="control-management" className="mt-0 min-h-0 flex-1 overflow-auto p-3">
|
|
||||||
<ControlManagementTab
|
|
||||||
screenId={currentScreenId}
|
|
||||||
groupId={groupId}
|
|
||||||
layoutItems={layoutItems}
|
|
||||||
loading={loading}
|
|
||||||
onRefresh={handleRefresh}
|
|
||||||
/>
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
{/* 탭 3: 데이터 흐름 */}
|
|
||||||
<TabsContent value="data-flow" className="mt-0 min-h-0 flex-1 overflow-auto p-3">
|
|
||||||
<DataFlowTab
|
|
||||||
screenId={currentScreenId}
|
|
||||||
groupId={groupId}
|
|
||||||
dataFlows={dataFlows}
|
|
||||||
loading={loading}
|
|
||||||
onReload={loadData}
|
|
||||||
onSaveSuccess={onSaveSuccess}
|
|
||||||
/>
|
|
||||||
</TabsContent>
|
|
||||||
</Tabs>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 오른쪽: 화면 프리뷰 (60%, 항상 표시) */}
|
|
||||||
<div className="flex min-h-0 w-[60%] flex-col overflow-hidden rounded-lg border bg-card">
|
|
||||||
<PreviewTab
|
<PreviewTab
|
||||||
screenId={currentScreenId}
|
screenId={currentScreenId}
|
||||||
screenName={currentScreenName}
|
screenName={currentScreenName}
|
||||||
|
|
|
||||||
|
|
@ -798,6 +798,25 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
|
||||||
minHeight: "48px",
|
minHeight: "48px",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{/* 테이블 선택 드롭다운 (여러 테이블 + showTableSelector 활성 시) */}
|
||||||
|
{showTableSelector && hasMultipleTables && (
|
||||||
|
<Select
|
||||||
|
value={selectedTableId || ""}
|
||||||
|
onValueChange={(value) => setSelectedTableId(value)}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="h-9 w-full shrink-0 text-xs sm:w-[200px] sm:text-sm">
|
||||||
|
<SelectValue placeholder="테이블 선택" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{tableList.map((table) => (
|
||||||
|
<SelectItem key={table.tableId} value={table.tableId}>
|
||||||
|
{table.label || table.tableName}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 필터 입력 필드들 */}
|
{/* 필터 입력 필드들 */}
|
||||||
{activeFilters.length > 0 && (
|
{activeFilters.length > 0 && (
|
||||||
<div className="flex flex-1 flex-col gap-2 sm:flex-row sm:flex-wrap sm:items-center">
|
<div className="flex flex-1 flex-col gap-2 sm:flex-row sm:flex-wrap sm:items-center">
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue