1672 lines
69 KiB
TypeScript
1672 lines
69 KiB
TypeScript
"use client";
|
|
|
|
/**
|
|
* 통합 설정 패널 (UnifiedConfigPanel) - Phase C: 스마트 Config
|
|
* - 테이블 선택 → 컬럼 자동 로드 → webType 자동 감지
|
|
* - 메타 컴포넌트 타입별 심플/고급 모드
|
|
* - API 클라이언트 사용 (fetch 금지)
|
|
*/
|
|
|
|
import React, { useState, useEffect } from "react";
|
|
import { cn } from "@/lib/utils";
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
import { Switch } from "@/components/ui/switch";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Separator } from "@/components/ui/separator";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Checkbox } from "@/components/ui/checkbox";
|
|
import { Settings2, Eye, EyeOff, Plus, Trash2, RefreshCw, Database, Palette, Link2, Settings, Wand2, SlidersHorizontal } from "lucide-react";
|
|
import { MetaComponent, getFieldConfig } from "@/lib/api/metaComponent";
|
|
import { apiClient } from "@/lib/api/client";
|
|
import { toast } from "sonner";
|
|
|
|
interface UnifiedConfigPanelProps {
|
|
component: MetaComponent | null;
|
|
onChange: (config: any) => void;
|
|
className?: string;
|
|
}
|
|
|
|
// 테이블 목록, 컬럼 목록 타입
|
|
interface TableInfo {
|
|
tableName: string;
|
|
displayName?: string;
|
|
description?: string;
|
|
}
|
|
|
|
interface ColumnInfo {
|
|
columnName: string;
|
|
dataType: string;
|
|
comment?: string;
|
|
}
|
|
|
|
// DB 타입 → webType 자동 매핑
|
|
function inferWebType(dbType: string): string {
|
|
const type = dbType.toLowerCase();
|
|
if (type.includes("varchar") || type.includes("text") || type.includes("char")) return "text";
|
|
if (type.includes("int") || type.includes("serial") || type.includes("bigint")) return "number";
|
|
if (type.includes("numeric") || type.includes("decimal") || type.includes("float") || type.includes("double")) return "number";
|
|
if (type.includes("bool")) return "checkbox";
|
|
if (type === "date") return "date";
|
|
if (type.includes("timestamp")) return "datetime";
|
|
if (type.includes("json")) return "textarea";
|
|
return "text";
|
|
}
|
|
|
|
export function UnifiedConfigPanel({ component, onChange, className }: UnifiedConfigPanelProps) {
|
|
const [isAdvanced, setIsAdvanced] = useState(false);
|
|
|
|
// Phase C: 테이블/컬럼 자동 로드 상태
|
|
const [tables, setTables] = useState<TableInfo[]>([]);
|
|
const [columns, setColumns] = useState<ColumnInfo[]>([]);
|
|
const [loadingTables, setLoadingTables] = useState(false);
|
|
const [loadingColumns, setLoadingColumns] = useState(false);
|
|
|
|
// Phase C: 컴포넌트 마운트 시 테이블 목록 로드
|
|
useEffect(() => {
|
|
loadTables();
|
|
}, []);
|
|
|
|
// Phase C: 테이블 선택 시 컬럼 로드
|
|
useEffect(() => {
|
|
if (component?.config?.tableName) {
|
|
loadColumns(component.config.tableName);
|
|
}
|
|
}, [component?.config?.tableName]);
|
|
|
|
// 테이블 목록 가져오기
|
|
const loadTables = async () => {
|
|
setLoadingTables(true);
|
|
try {
|
|
const response = await apiClient.get("/table-management/tables");
|
|
const data = response.data?.data || [];
|
|
setTables(data);
|
|
} catch (error: any) {
|
|
console.error("테이블 목록 로드 실패:", error);
|
|
toast.error("테이블 목록을 불러올 수 없습니다");
|
|
} finally {
|
|
setLoadingTables(false);
|
|
}
|
|
};
|
|
|
|
// 특정 테이블의 컬럼 목록 가져오기
|
|
const loadColumns = async (tableName: string) => {
|
|
if (!tableName) return;
|
|
setLoadingColumns(true);
|
|
try {
|
|
const response = await apiClient.get(`/table-management/tables/${tableName}/columns?size=1000`);
|
|
const data = response.data?.data || response.data;
|
|
const cols = data.columns || data || [];
|
|
setColumns(cols);
|
|
} catch (error: any) {
|
|
console.error("컬럼 목록 로드 실패:", error);
|
|
toast.error("컬럼 목록을 불러올 수 없습니다");
|
|
setColumns([]);
|
|
} finally {
|
|
setLoadingColumns(false);
|
|
}
|
|
};
|
|
|
|
if (!component) {
|
|
return (
|
|
<div className={cn("flex h-full items-center justify-center bg-muted/10", className)}>
|
|
<div className="text-center">
|
|
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-primary/10">
|
|
<Settings2 className="h-8 w-8 text-primary" />
|
|
</div>
|
|
<h3 className="mb-2 text-base font-semibold text-foreground">컴포넌트를 선택하세요</h3>
|
|
<p className="text-muted-foreground text-xs">캔버스에서 컴포넌트를 클릭하면 여기에서 설정할 수 있습니다</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const { type, config } = component;
|
|
|
|
// 설정 변경 핸들러
|
|
const handleConfigChange = (key: string, value: any) => {
|
|
onChange({ ...config, [key]: value });
|
|
};
|
|
|
|
// 중첩 설정 변경 핸들러 (예: text.content)
|
|
const handleNestedChange = (path: string[], value: any) => {
|
|
const newConfig = { ...config };
|
|
let current: any = newConfig;
|
|
for (let i = 0; i < path.length - 1; i++) {
|
|
if (!current[path[i]]) current[path[i]] = {};
|
|
current = current[path[i]];
|
|
}
|
|
current[path[path.length - 1]] = value;
|
|
onChange(newConfig);
|
|
};
|
|
|
|
// 공통 기본 설정 (모든 컴포넌트)
|
|
const renderBasicTab = () => (
|
|
<TabsContent value="basic" className="space-y-4">
|
|
{/* 컴포넌트 유형 섹션 */}
|
|
<div className="space-y-3">
|
|
<div className="flex items-center justify-between">
|
|
<Label className="text-xs font-medium text-muted-foreground">컴포넌트 유형</Label>
|
|
<Badge variant="outline" className="text-xs">{type}</Badge>
|
|
</div>
|
|
<Separator />
|
|
</div>
|
|
|
|
{/* Field 컴포넌트 설정 - Phase C: 스마트 설정 */}
|
|
{type === "meta-field" && (
|
|
<>
|
|
{/* 심플 모드: 테이블 → 컬럼 → 라벨 → webType */}
|
|
<div className="space-y-3">
|
|
<Label className="text-xs font-medium text-muted-foreground">테이블 선택 *</Label>
|
|
<Select
|
|
value={(config as any).tableName || ""}
|
|
onValueChange={(tableName) => {
|
|
handleConfigChange("tableName", tableName);
|
|
// 테이블 선택하면 컬럼 초기화
|
|
handleConfigChange("binding", "");
|
|
handleConfigChange("label", "");
|
|
}}
|
|
>
|
|
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
|
|
<SelectValue placeholder={loadingTables ? "로딩 중..." : "테이블 선택"} />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{tables.map((table) => (
|
|
<SelectItem key={table.tableName} value={table.tableName}>
|
|
{table.displayName || table.tableName}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{(config as any).tableName && (
|
|
<div className="space-y-3">
|
|
<Label className="text-xs font-medium text-muted-foreground">컬럼 선택 *</Label>
|
|
<Select
|
|
value={(config as any).binding || ""}
|
|
onValueChange={(columnName) => {
|
|
handleConfigChange("binding", columnName);
|
|
// 컬럼 선택 시 자동으로 라벨과 webType 설정
|
|
const selectedCol = columns.find((c) => c.columnName === columnName);
|
|
if (selectedCol) {
|
|
const label = selectedCol.comment || columnName;
|
|
const webType = inferWebType(selectedCol.dataType);
|
|
handleConfigChange("label", label);
|
|
handleConfigChange("webType", webType);
|
|
toast.success(`라벨: ${label}, 타입: ${webType}`);
|
|
}
|
|
}}
|
|
>
|
|
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
|
|
<SelectValue placeholder={loadingColumns ? "로딩 중..." : "컬럼 선택"} />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{columns.map((col) => (
|
|
<SelectItem key={col.columnName} value={col.columnName}>
|
|
{col.comment ? `${col.comment} (${col.columnName})` : col.columnName}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
)}
|
|
|
|
<div className="space-y-3">
|
|
<Label className="text-xs font-medium text-muted-foreground">라벨</Label>
|
|
<Input
|
|
value={(config as any).label || ""}
|
|
onChange={(e) => handleConfigChange("label", e.target.value)}
|
|
placeholder="예: 고객명, 주문번호..."
|
|
className="h-8 text-xs sm:h-10 sm:text-sm"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-3">
|
|
<Label className="text-xs font-medium text-muted-foreground">필드 타입</Label>
|
|
<Select value={(config as any).webType || "text"} onValueChange={(v) => handleConfigChange("webType", v)}>
|
|
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
|
|
<SelectValue placeholder="필드 타입 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="text">텍스트</SelectItem>
|
|
<SelectItem value="number">숫자</SelectItem>
|
|
<SelectItem value="date">날짜</SelectItem>
|
|
<SelectItem value="datetime">날짜시간</SelectItem>
|
|
<SelectItem value="checkbox">체크박스</SelectItem>
|
|
<SelectItem value="textarea">텍스트 영역</SelectItem>
|
|
<SelectItem value="select">선택</SelectItem>
|
|
<SelectItem value="entity">엔티티</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{isAdvanced && (
|
|
<>
|
|
<Separator className="my-4" />
|
|
<div className="space-y-3">
|
|
<Label className="text-xs font-medium text-muted-foreground">플레이스홀더</Label>
|
|
<Input
|
|
value={(config as any).placeholder || ""}
|
|
onChange={(e) => handleConfigChange("placeholder", e.target.value)}
|
|
placeholder="입력 필드에 표시될 힌트 텍스트"
|
|
className="h-8 text-xs sm:h-10 sm:text-sm"
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between rounded-lg border p-3">
|
|
<Label className="text-xs font-medium text-muted-foreground">필수 입력</Label>
|
|
<Switch
|
|
checked={(config as any).required || false}
|
|
onCheckedChange={(checked) => handleConfigChange("required", checked)}
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between rounded-lg border p-3">
|
|
<Label className="text-xs font-medium text-muted-foreground">읽기 전용</Label>
|
|
<Switch
|
|
checked={(config as any).readonly || false}
|
|
onCheckedChange={(checked) => handleConfigChange("readonly", checked)}
|
|
/>
|
|
</div>
|
|
</>
|
|
)}
|
|
</>
|
|
)}
|
|
|
|
{/* DataView 컴포넌트 설정 - Phase C */}
|
|
{type === "meta-dataview" && (
|
|
<>
|
|
{/* 심플 모드: 테이블 → 페이지 크기 */}
|
|
<div className="space-y-3">
|
|
<Label className="text-xs font-medium text-muted-foreground">테이블 선택 *</Label>
|
|
<Select
|
|
value={(config as any).tableName || ""}
|
|
onValueChange={(tableName) => {
|
|
handleConfigChange("tableName", tableName);
|
|
handleConfigChange("columns", ""); // 컬럼 선택 초기화
|
|
}}
|
|
>
|
|
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
|
|
<SelectValue placeholder={loadingTables ? "로딩 중..." : "테이블 선택"} />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{tables.map((table) => (
|
|
<SelectItem key={table.tableName} value={table.tableName}>
|
|
{table.displayName || table.tableName}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div className="space-y-3">
|
|
<Label className="text-xs font-medium text-muted-foreground">뷰 모드</Label>
|
|
<Select value={(config as any).viewMode || "table"} onValueChange={(v) => handleConfigChange("viewMode", v)}>
|
|
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
|
|
<SelectValue placeholder="뷰 모드 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="table">테이블</SelectItem>
|
|
<SelectItem value="card">카드</SelectItem>
|
|
<SelectItem value="list">리스트</SelectItem>
|
|
<SelectItem value="tree">트리</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div className="space-y-3">
|
|
<Label className="text-xs font-medium text-muted-foreground">페이지 크기</Label>
|
|
<Input
|
|
type="number"
|
|
value={(config as any).pageSize || 10}
|
|
onChange={(e) => handleConfigChange("pageSize", parseInt(e.target.value) || 10)}
|
|
placeholder="10"
|
|
className="h-8 text-xs sm:h-10 sm:text-sm"
|
|
/>
|
|
</div>
|
|
|
|
{isAdvanced && (config as any).tableName && (
|
|
<>
|
|
<Separator className="my-4" />
|
|
<div className="space-y-3">
|
|
<Label className="text-xs font-medium text-muted-foreground">표시 컬럼 선택</Label>
|
|
<p className="text-[10px] text-muted-foreground">체크박스로 표시할 컬럼을 선택하세요</p>
|
|
<div className="max-h-[200px] space-y-2 overflow-y-auto rounded border p-3">
|
|
{loadingColumns ? (
|
|
<p className="text-xs text-muted-foreground">로딩 중...</p>
|
|
) : columns.length === 0 ? (
|
|
<p className="text-xs text-muted-foreground">컬럼이 없습니다</p>
|
|
) : (
|
|
columns.map((col) => {
|
|
const selectedColumns = ((config as any).columns || "").split(",").filter(Boolean);
|
|
const isChecked = selectedColumns.includes(col.columnName);
|
|
return (
|
|
<div key={col.columnName} className="flex items-center gap-2">
|
|
<Checkbox
|
|
checked={isChecked}
|
|
onCheckedChange={(checked) => {
|
|
let updatedColumns = [...selectedColumns];
|
|
if (checked) {
|
|
updatedColumns.push(col.columnName);
|
|
} else {
|
|
updatedColumns = updatedColumns.filter((c) => c !== col.columnName);
|
|
}
|
|
handleConfigChange("columns", updatedColumns.join(","));
|
|
}}
|
|
/>
|
|
<Label className="text-xs">
|
|
{col.comment ? `${col.comment} (${col.columnName})` : col.columnName}
|
|
</Label>
|
|
</div>
|
|
);
|
|
})
|
|
)}
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
</>
|
|
)}
|
|
|
|
{/* Action 컴포넌트 설정 - Phase C */}
|
|
{type === "meta-action" && (
|
|
<>
|
|
{/* 심플 모드: 액션 유형 → 대상 테이블 → 버튼 텍스트 → 버튼 스타일 */}
|
|
<div className="space-y-3">
|
|
<Label className="text-xs font-medium text-muted-foreground">액션 유형</Label>
|
|
<Select
|
|
value={(config as any).actionType || "save"}
|
|
onValueChange={(v) => handleConfigChange("actionType", v)}
|
|
>
|
|
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
|
|
<SelectValue placeholder="액션 유형 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="save">저장</SelectItem>
|
|
<SelectItem value="delete">삭제</SelectItem>
|
|
<SelectItem value="refresh">새로고침</SelectItem>
|
|
<SelectItem value="custom">커스텀</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div className="space-y-3">
|
|
<Label className="text-xs font-medium text-muted-foreground">대상 테이블 (선택)</Label>
|
|
<Select
|
|
value={(config as any).targetTable || ""}
|
|
onValueChange={(v) => handleConfigChange("targetTable", v)}
|
|
>
|
|
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
|
|
<SelectValue placeholder={loadingTables ? "로딩 중..." : "테이블 선택"} />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="">선택 안 함</SelectItem>
|
|
{tables.map((table) => (
|
|
<SelectItem key={table.tableName} value={table.tableName}>
|
|
{table.displayName || table.tableName}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div className="space-y-3">
|
|
<Label className="text-xs font-medium text-muted-foreground">버튼 라벨</Label>
|
|
<Input
|
|
value={(config as any).label || ""}
|
|
onChange={(e) => handleConfigChange("label", e.target.value)}
|
|
placeholder="예: 저장, 삭제, 조회..."
|
|
className="h-8 text-xs sm:h-10 sm:text-sm"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-3">
|
|
<Label className="text-xs font-medium text-muted-foreground">버튼 스타일</Label>
|
|
<Select
|
|
value={(config as any).buttonType || "primary"}
|
|
onValueChange={(v) => handleConfigChange("buttonType", v)}
|
|
>
|
|
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
|
|
<SelectValue placeholder="버튼 스타일 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="primary">Primary</SelectItem>
|
|
<SelectItem value="secondary">Secondary</SelectItem>
|
|
<SelectItem value="destructive">Destructive</SelectItem>
|
|
<SelectItem value="ghost">Ghost</SelectItem>
|
|
<SelectItem value="outline">Outline</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{isAdvanced && (
|
|
<>
|
|
<Separator className="my-4" />
|
|
<div className="flex items-center justify-between rounded-lg border p-3">
|
|
<Label className="text-xs font-medium text-muted-foreground">확인 다이얼로그</Label>
|
|
<Switch
|
|
checked={(config as any).confirmDialog || false}
|
|
onCheckedChange={(checked) => handleConfigChange("confirmDialog", checked)}
|
|
/>
|
|
</div>
|
|
</>
|
|
)}
|
|
</>
|
|
)}
|
|
|
|
{/* Layout 컴포넌트 설정 */}
|
|
{type === "meta-layout" && (
|
|
<>
|
|
<div className="space-y-3">
|
|
<Label className="text-xs font-medium text-muted-foreground">레이아웃 모드</Label>
|
|
<Select value={(config as any).mode} onValueChange={(v) => handleConfigChange("mode", v)}>
|
|
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
|
|
<SelectValue placeholder="레이아웃 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="columns">컬럼</SelectItem>
|
|
<SelectItem value="rows">로우</SelectItem>
|
|
<SelectItem value="tabs">탭</SelectItem>
|
|
<SelectItem value="accordion">아코디언</SelectItem>
|
|
<SelectItem value="card">카드</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{isAdvanced && (
|
|
<>
|
|
<Separator className="my-4" />
|
|
<div className="space-y-3">
|
|
<Label className="text-xs font-medium text-muted-foreground">간격 (gap)</Label>
|
|
<Input
|
|
type="number"
|
|
value={(config as any).gap || 4}
|
|
onChange={(e) => handleConfigChange("gap", parseInt(e.target.value))}
|
|
placeholder="4"
|
|
className="h-8 text-xs sm:h-10 sm:text-sm"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-3">
|
|
<Label className="text-xs font-medium text-muted-foreground">패딩 (padding)</Label>
|
|
<Input
|
|
type="number"
|
|
value={(config as any).padding || 4}
|
|
onChange={(e) => handleConfigChange("padding", parseInt(e.target.value))}
|
|
placeholder="4"
|
|
className="h-8 text-xs sm:h-10 sm:text-sm"
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between rounded-lg border p-3">
|
|
<Label className="text-xs font-medium text-muted-foreground">테두리</Label>
|
|
<Switch
|
|
checked={(config as any).bordered || false}
|
|
onCheckedChange={(checked) => handleConfigChange("bordered", checked)}
|
|
/>
|
|
</div>
|
|
</>
|
|
)}
|
|
</>
|
|
)}
|
|
|
|
{/* Display 컴포넌트 설정 */}
|
|
{type === "meta-display" && (
|
|
<>
|
|
<div className="space-y-3">
|
|
<Label className="text-xs font-medium text-muted-foreground">표시 타입</Label>
|
|
<Select value={(config as any).displayType} onValueChange={(v) => handleConfigChange("displayType", v)}>
|
|
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
|
|
<SelectValue placeholder="표시 타입 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="text">텍스트</SelectItem>
|
|
<SelectItem value="heading">제목</SelectItem>
|
|
<SelectItem value="divider">구분선</SelectItem>
|
|
<SelectItem value="badge">배지</SelectItem>
|
|
<SelectItem value="alert">알림</SelectItem>
|
|
<SelectItem value="stat">통계</SelectItem>
|
|
<SelectItem value="spacer">공백</SelectItem>
|
|
<SelectItem value="progress">진행률</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{(config as any).displayType === "text" && (
|
|
<div className="space-y-3">
|
|
<Label className="text-xs font-medium text-muted-foreground">내용</Label>
|
|
<Input
|
|
value={(config as any).text?.content || ""}
|
|
onChange={(e) => handleNestedChange(["text", "content"], e.target.value)}
|
|
placeholder="표시할 텍스트를 입력하세요"
|
|
className="h-8 text-xs sm:h-10 sm:text-sm"
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{(config as any).displayType === "heading" && (
|
|
<>
|
|
<div className="space-y-3">
|
|
<Label className="text-xs font-medium text-muted-foreground">내용</Label>
|
|
<Input
|
|
value={(config as any).heading?.content || ""}
|
|
onChange={(e) => handleNestedChange(["heading", "content"], e.target.value)}
|
|
placeholder="제목 텍스트를 입력하세요"
|
|
className="h-8 text-xs sm:h-10 sm:text-sm"
|
|
/>
|
|
</div>
|
|
<div className="space-y-3">
|
|
<Label className="text-xs font-medium text-muted-foreground">레벨</Label>
|
|
<Select
|
|
value={String((config as any).heading?.level || 2)}
|
|
onValueChange={(v) => handleNestedChange(["heading", "level"], parseInt(v))}
|
|
>
|
|
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{[1, 2, 3, 4, 5, 6].map((level) => (
|
|
<SelectItem key={level} value={String(level)}>
|
|
H{level}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{(config as any).displayType === "stat" && (
|
|
<>
|
|
<div className="space-y-3">
|
|
<Label className="text-xs font-medium text-muted-foreground">값</Label>
|
|
<Input
|
|
value={(config as any).stat?.value || ""}
|
|
onChange={(e) => handleNestedChange(["stat", "value"], e.target.value)}
|
|
placeholder="예: 1,234"
|
|
className="h-8 text-xs sm:h-10 sm:text-sm"
|
|
/>
|
|
</div>
|
|
<div className="space-y-3">
|
|
<Label className="text-xs font-medium text-muted-foreground">라벨</Label>
|
|
<Input
|
|
value={(config as any).stat?.label || ""}
|
|
onChange={(e) => handleNestedChange(["stat", "label"], e.target.value)}
|
|
placeholder="예: 총 판매액"
|
|
className="h-8 text-xs sm:h-10 sm:text-sm"
|
|
/>
|
|
</div>
|
|
</>
|
|
)}
|
|
</>
|
|
)}
|
|
|
|
{/* Search 컴포넌트 설정 - Phase C */}
|
|
{type === "meta-search" && (
|
|
<>
|
|
{/* 심플 모드: 대상 DataView → 검색 필드 추가 */}
|
|
<div className="space-y-3">
|
|
<Label className="text-xs font-medium text-muted-foreground">검색 모드</Label>
|
|
<Select value={(config as any).mode || "simple"} onValueChange={(v) => handleConfigChange("mode", v)}>
|
|
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
|
|
<SelectValue placeholder="검색 모드 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="simple">Simple</SelectItem>
|
|
<SelectItem value="advanced">Advanced</SelectItem>
|
|
<SelectItem value="combined">Combined</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div className="space-y-3">
|
|
<Label className="text-xs font-medium text-muted-foreground">대상 DataView ID</Label>
|
|
<Input
|
|
value={(config as any).targetDataView || ""}
|
|
onChange={(e) => handleConfigChange("targetDataView", e.target.value)}
|
|
placeholder="예: dataview-1"
|
|
className="h-8 text-xs sm:h-10 sm:text-sm"
|
|
/>
|
|
<p className="text-[10px] text-muted-foreground">
|
|
같은 화면의 DataView 컴포넌트 ID를 입력하세요
|
|
</p>
|
|
</div>
|
|
|
|
<Separator className="my-4" />
|
|
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<Label className="text-xs font-medium">검색 필드</Label>
|
|
<p className="mt-1 text-[10px] text-muted-foreground">검색할 컬럼을 추가하세요</p>
|
|
</div>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => {
|
|
const fields = (config as any).searchFields || [];
|
|
handleConfigChange("searchFields", [
|
|
...fields,
|
|
{ column: "", label: "", searchType: "contains" },
|
|
]);
|
|
}}
|
|
>
|
|
<Plus className="mr-1 h-4 w-4" />
|
|
필드 추가
|
|
</Button>
|
|
</div>
|
|
|
|
{((config as any).searchFields || []).map((field: any, idx: number) => (
|
|
<div key={idx} className="space-y-2 rounded border p-3">
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-xs font-medium">필드 {idx + 1}</span>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => {
|
|
const fields = [...(config as any).searchFields];
|
|
fields.splice(idx, 1);
|
|
handleConfigChange("searchFields", fields);
|
|
}}
|
|
>
|
|
<Trash2 className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
|
|
<Input
|
|
value={field.column || ""}
|
|
onChange={(e) => {
|
|
const fields = [...(config as any).searchFields];
|
|
fields[idx].column = e.target.value;
|
|
handleConfigChange("searchFields", fields);
|
|
}}
|
|
placeholder="컬럼명 (예: customer_name)"
|
|
className="h-8 text-xs sm:h-10 sm:text-sm"
|
|
/>
|
|
|
|
<Input
|
|
value={field.label || ""}
|
|
onChange={(e) => {
|
|
const fields = [...(config as any).searchFields];
|
|
fields[idx].label = e.target.value;
|
|
handleConfigChange("searchFields", fields);
|
|
}}
|
|
placeholder="라벨 (예: 고객명)"
|
|
className="h-8 text-xs sm:h-10 sm:text-sm"
|
|
/>
|
|
|
|
{isAdvanced && (
|
|
<Select
|
|
value={field.searchType || "contains"}
|
|
onValueChange={(v) => {
|
|
const fields = [...(config as any).searchFields];
|
|
fields[idx].searchType = v;
|
|
handleConfigChange("searchFields", fields);
|
|
}}
|
|
>
|
|
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="contains">포함</SelectItem>
|
|
<SelectItem value="equals">일치</SelectItem>
|
|
<SelectItem value="startsWith">시작</SelectItem>
|
|
<SelectItem value="endsWith">끝</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
)}
|
|
</div>
|
|
))}
|
|
</>
|
|
)}
|
|
|
|
{/* Modal 컴포넌트 설정 */}
|
|
{type === "meta-modal" && (
|
|
<>
|
|
<div className="space-y-3">
|
|
<Label className="text-xs font-medium text-muted-foreground">트리거</Label>
|
|
<Select value={(config as any).trigger} onValueChange={(v) => handleConfigChange("trigger", v)}>
|
|
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
|
|
<SelectValue placeholder="모달 열림 방식 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="button">버튼</SelectItem>
|
|
<SelectItem value="row_click">행 클릭</SelectItem>
|
|
<SelectItem value="row_double_click">행 더블클릭</SelectItem>
|
|
<SelectItem value="action">액션</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{(config as any).trigger === "button" && (
|
|
<div className="space-y-3">
|
|
<Label className="text-xs font-medium text-muted-foreground">버튼 라벨</Label>
|
|
<Input
|
|
value={(config as any).triggerLabel || ""}
|
|
onChange={(e) => handleConfigChange("triggerLabel", e.target.value)}
|
|
placeholder="예: 상세보기, 추가..."
|
|
className="h-8 text-xs sm:h-10 sm:text-sm"
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
<div className="space-y-3">
|
|
<Label className="text-xs font-medium text-muted-foreground">모달 크기</Label>
|
|
<Select value={(config as any).size || "md"} onValueChange={(v) => handleConfigChange("size", v)}>
|
|
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="sm">Small</SelectItem>
|
|
<SelectItem value="md">Medium</SelectItem>
|
|
<SelectItem value="lg">Large</SelectItem>
|
|
<SelectItem value="xl">Extra Large</SelectItem>
|
|
<SelectItem value="full">Full</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</>
|
|
)}
|
|
</TabsContent>
|
|
);
|
|
|
|
// 데이터 탭
|
|
const renderDataTab = () => {
|
|
// DB 설정 가져오기 핸들러 (기존 유지)
|
|
const handleFetchFieldConfig = async () => {
|
|
const tableName = (config as any).tableName;
|
|
const columnName = (config as any).binding;
|
|
|
|
if (!tableName || !columnName) {
|
|
toast.error("테이블명과 컬럼명을 먼저 입력하세요");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await getFieldConfig(tableName, columnName);
|
|
if (response.success && response.data) {
|
|
// DB 설정으로 config 자동 채우기
|
|
const dbConfig = response.data;
|
|
onChange({
|
|
...config,
|
|
webType: dbConfig.webType || (config as any).webType,
|
|
label: dbConfig.label || (config as any).label,
|
|
required: dbConfig.required !== undefined ? dbConfig.required : (config as any).required,
|
|
options: dbConfig.options || (config as any).options,
|
|
join: dbConfig.join || (config as any).join,
|
|
});
|
|
toast.success("DB 설정을 가져왔습니다");
|
|
} else {
|
|
toast.error(response.error || "설정을 가져올 수 없습니다");
|
|
}
|
|
} catch (error: any) {
|
|
toast.error("DB 설정 가져오기 실패: " + error.message);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<TabsContent value="data" className="space-y-4">
|
|
{/* meta-field */}
|
|
{type === "meta-field" && (
|
|
<>
|
|
<div className="space-y-3">
|
|
<Label className="text-xs font-medium text-muted-foreground">기본값</Label>
|
|
<Input
|
|
value={(config as any).defaultValue || ""}
|
|
onChange={(e) => handleConfigChange("defaultValue", e.target.value)}
|
|
placeholder="필드 초기값"
|
|
className="h-8 text-xs sm:h-10 sm:text-sm"
|
|
/>
|
|
</div>
|
|
|
|
<Separator className="my-4" />
|
|
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={handleFetchFieldConfig}
|
|
className="w-full"
|
|
>
|
|
<RefreshCw className="mr-2 h-4 w-4" />
|
|
DB에서 설정 가져오기
|
|
</Button>
|
|
</>
|
|
)}
|
|
|
|
{/* meta-dataview - Phase C: 데이터 탭에서는 정렬/페이징만 */}
|
|
{type === "meta-dataview" && (
|
|
<>
|
|
<div className="space-y-3">
|
|
<Label className="text-xs font-medium text-muted-foreground">기본 정렬 컬럼</Label>
|
|
<Input
|
|
value={(config as any).defaultSortColumn || ""}
|
|
onChange={(e) => handleConfigChange("defaultSortColumn", e.target.value)}
|
|
placeholder="예: created_at"
|
|
className="h-8 text-xs sm:h-10 sm:text-sm"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-3">
|
|
<Label className="text-xs font-medium text-muted-foreground">기본 정렬 방향</Label>
|
|
<Select
|
|
value={(config as any).defaultSortDirection || "desc"}
|
|
onValueChange={(v) => handleConfigChange("defaultSortDirection", v)}
|
|
>
|
|
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="asc">오름차순</SelectItem>
|
|
<SelectItem value="desc">내림차순</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{/* meta-modal (form 타입) - Phase C: 심플하게 */}
|
|
{type === "meta-modal" && (
|
|
<>
|
|
<div className="space-y-3">
|
|
<Label className="text-xs font-medium text-muted-foreground">테이블명</Label>
|
|
<Select
|
|
value={(config as any).tableName || ""}
|
|
onValueChange={(v) => handleConfigChange("tableName", v)}
|
|
>
|
|
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
|
|
<SelectValue placeholder={loadingTables ? "로딩 중..." : "테이블 선택"} />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{tables.map((table) => (
|
|
<SelectItem key={table.tableName} value={table.tableName}>
|
|
{table.displayName || table.tableName}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div className="space-y-3">
|
|
<Label className="text-xs font-medium text-muted-foreground">모드</Label>
|
|
<Select
|
|
value={(config as any).mode || "create"}
|
|
onValueChange={(v) => handleConfigChange("mode", v)}
|
|
>
|
|
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="create">생성</SelectItem>
|
|
<SelectItem value="edit">수정</SelectItem>
|
|
<SelectItem value="view">조회</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{isAdvanced && (
|
|
<>
|
|
<div className="space-y-3">
|
|
<Label className="text-xs font-medium text-muted-foreground">레이아웃</Label>
|
|
<Select
|
|
value={(config as any).formLayout || "single"}
|
|
onValueChange={(v) => handleConfigChange("formLayout", v)}
|
|
>
|
|
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="single">단일 컬럼</SelectItem>
|
|
<SelectItem value="two_column">2단 컬럼</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<Separator className="my-4" />
|
|
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<Label className="text-xs font-medium">폼 컬럼</Label>
|
|
<p className="mt-1 text-[10px] text-muted-foreground">폼에 표시할 필드를 추가하세요</p>
|
|
</div>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => {
|
|
const cols = (config as any).formColumns || [];
|
|
handleConfigChange("formColumns", [
|
|
...cols,
|
|
{ column: "", label: "", type: "text" },
|
|
]);
|
|
}}
|
|
>
|
|
<Plus className="mr-1 h-4 w-4" />
|
|
컬럼 추가
|
|
</Button>
|
|
</div>
|
|
|
|
{((config as any).formColumns || []).map((col: any, idx: number) => (
|
|
<div key={idx} className="space-y-2 rounded border p-3">
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-xs font-medium">컬럼 {idx + 1}</span>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => {
|
|
const cols = [...(config as any).formColumns];
|
|
cols.splice(idx, 1);
|
|
handleConfigChange("formColumns", cols);
|
|
}}
|
|
>
|
|
<Trash2 className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
|
|
<Input
|
|
value={col.column || ""}
|
|
onChange={(e) => {
|
|
const cols = [...(config as any).formColumns];
|
|
cols[idx].column = e.target.value;
|
|
handleConfigChange("formColumns", cols);
|
|
}}
|
|
placeholder="컬럼명"
|
|
className="h-8 text-xs sm:h-10 sm:text-sm"
|
|
/>
|
|
|
|
<Input
|
|
value={col.label || ""}
|
|
onChange={(e) => {
|
|
const cols = [...(config as any).formColumns];
|
|
cols[idx].label = e.target.value;
|
|
handleConfigChange("formColumns", cols);
|
|
}}
|
|
placeholder="라벨"
|
|
className="h-8 text-xs sm:h-10 sm:text-sm"
|
|
/>
|
|
|
|
<Select
|
|
value={col.type || "text"}
|
|
onValueChange={(v) => {
|
|
const cols = [...(config as any).formColumns];
|
|
cols[idx].type = v;
|
|
handleConfigChange("formColumns", cols);
|
|
}}
|
|
>
|
|
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="text">텍스트</SelectItem>
|
|
<SelectItem value="number">숫자</SelectItem>
|
|
<SelectItem value="date">날짜</SelectItem>
|
|
<SelectItem value="select">선택</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
))}
|
|
</>
|
|
)}
|
|
</>
|
|
)}
|
|
|
|
{/* 기타 컴포넌트 - 데이터 설정 불필요 */}
|
|
{!["meta-field", "meta-dataview", "meta-modal"].includes(type) && (
|
|
<div className="flex flex-col items-center justify-center py-12 text-center">
|
|
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-muted">
|
|
<Database className="h-8 w-8 text-muted-foreground" />
|
|
</div>
|
|
<h4 className="mb-2 text-sm font-medium">데이터 설정 불필요</h4>
|
|
<p className="max-w-[250px] text-xs text-muted-foreground">
|
|
이 컴포넌트는 데이터베이스 설정이 필요하지 않습니다.
|
|
</p>
|
|
</div>
|
|
)}
|
|
</TabsContent>
|
|
);
|
|
};
|
|
|
|
// 표시 탭
|
|
const renderDisplayTab = () => (
|
|
<TabsContent value="display" className="space-y-4">
|
|
{/* 공통 크기 설정 섹션 */}
|
|
<div className="space-y-3">
|
|
<div className="flex items-center gap-2">
|
|
<Label className="text-xs font-medium">크기 설정</Label>
|
|
</div>
|
|
<Separator className="mb-4" />
|
|
|
|
<div className="space-y-3">
|
|
<Label className="text-xs font-medium text-muted-foreground">너비</Label>
|
|
<Input
|
|
value={(config as any).width || ""}
|
|
onChange={(e) => handleConfigChange("width", e.target.value)}
|
|
placeholder="예: 100%, 400px, auto"
|
|
className="h-8 text-xs sm:h-10 sm:text-sm"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-3">
|
|
<Label className="text-xs font-medium text-muted-foreground">높이</Label>
|
|
<Input
|
|
value={(config as any).height || ""}
|
|
onChange={(e) => handleConfigChange("height", e.target.value)}
|
|
placeholder="예: 200px, auto"
|
|
className="h-8 text-xs sm:h-10 sm:text-sm"
|
|
/>
|
|
</div>
|
|
|
|
{isAdvanced && (
|
|
<>
|
|
<div className="space-y-3">
|
|
<Label className="text-xs font-medium text-muted-foreground">최소 너비</Label>
|
|
<Input
|
|
value={(config as any).minWidth || ""}
|
|
onChange={(e) => handleConfigChange("minWidth", e.target.value)}
|
|
placeholder="예: 200px"
|
|
className="h-8 text-xs sm:h-10 sm:text-sm"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-3">
|
|
<Label className="text-xs font-medium text-muted-foreground">최대 너비</Label>
|
|
<Input
|
|
value={(config as any).maxWidth || ""}
|
|
onChange={(e) => handleConfigChange("maxWidth", e.target.value)}
|
|
placeholder="예: 1200px"
|
|
className="h-8 text-xs sm:h-10 sm:text-sm"
|
|
/>
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
|
|
<Separator className="my-6" />
|
|
|
|
{/* meta-field 전용 */}
|
|
{type === "meta-field" && (
|
|
<>
|
|
<div className="space-y-3">
|
|
<div className="flex items-center gap-2">
|
|
<Label className="text-xs font-medium">라벨 설정</Label>
|
|
</div>
|
|
<Separator className="mb-4" />
|
|
|
|
<div className="space-y-3">
|
|
<Label className="text-xs font-medium text-muted-foreground">라벨 위치</Label>
|
|
<Select
|
|
value={(config as any).labelPosition || "top"}
|
|
onValueChange={(v) => handleConfigChange("labelPosition", v)}
|
|
>
|
|
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="top">위쪽</SelectItem>
|
|
<SelectItem value="left">왼쪽</SelectItem>
|
|
<SelectItem value="hidden">숨김</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{(config as any).labelPosition === "left" && (
|
|
<div className="space-y-3">
|
|
<Label className="text-xs font-medium text-muted-foreground">라벨 너비</Label>
|
|
<Input
|
|
value={(config as any).labelWidth || "120px"}
|
|
onChange={(e) => handleConfigChange("labelWidth", e.target.value)}
|
|
placeholder="예: 120px"
|
|
className="h-8 text-xs sm:h-10 sm:text-sm"
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{/* meta-display 전용 */}
|
|
{type === "meta-display" && (
|
|
<>
|
|
<div className="space-y-3">
|
|
<div className="flex items-center gap-2">
|
|
<Label className="text-xs font-medium">텍스트 스타일</Label>
|
|
</div>
|
|
<Separator className="mb-4" />
|
|
|
|
<div className="space-y-3">
|
|
<Label className="text-xs font-medium text-muted-foreground">텍스트 크기</Label>
|
|
<Select
|
|
value={(config as any).textSize || "md"}
|
|
onValueChange={(v) => handleConfigChange("textSize", v)}
|
|
>
|
|
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="xs">XS</SelectItem>
|
|
<SelectItem value="sm">SM</SelectItem>
|
|
<SelectItem value="md">MD</SelectItem>
|
|
<SelectItem value="lg">LG</SelectItem>
|
|
<SelectItem value="xl">XL</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div className="space-y-3">
|
|
<Label className="text-xs font-medium text-muted-foreground">텍스트 굵기</Label>
|
|
<Select
|
|
value={(config as any).fontWeight || "normal"}
|
|
onValueChange={(v) => handleConfigChange("fontWeight", v)}
|
|
>
|
|
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="normal">Normal</SelectItem>
|
|
<SelectItem value="medium">Medium</SelectItem>
|
|
<SelectItem value="semibold">Semibold</SelectItem>
|
|
<SelectItem value="bold">Bold</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div className="space-y-3">
|
|
<Label className="text-xs font-medium text-muted-foreground">정렬</Label>
|
|
<Select
|
|
value={(config as any).textAlign || "left"}
|
|
onValueChange={(v) => handleConfigChange("textAlign", v)}
|
|
>
|
|
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="left">왼쪽</SelectItem>
|
|
<SelectItem value="center">가운데</SelectItem>
|
|
<SelectItem value="right">오른쪽</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{/* meta-layout 전용 */}
|
|
{type === "meta-layout" && isAdvanced && (
|
|
<>
|
|
<div className="space-y-3">
|
|
<div className="flex items-center gap-2">
|
|
<Label className="text-xs font-medium">레이아웃 여백</Label>
|
|
</div>
|
|
<Separator className="mb-4" />
|
|
|
|
<div className="space-y-3">
|
|
<Label className="text-xs font-medium text-muted-foreground">간격 (gap)</Label>
|
|
<Input
|
|
type="number"
|
|
value={(config as any).gap || 4}
|
|
onChange={(e) => handleConfigChange("gap", parseInt(e.target.value))}
|
|
placeholder="4"
|
|
className="h-8 text-xs sm:h-10 sm:text-sm"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-3">
|
|
<Label className="text-xs font-medium text-muted-foreground">패딩 (padding)</Label>
|
|
<Input
|
|
type="number"
|
|
value={(config as any).padding || 4}
|
|
onChange={(e) => handleConfigChange("padding", parseInt(e.target.value))}
|
|
placeholder="4"
|
|
className="h-8 text-xs sm:h-10 sm:text-sm"
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between rounded-lg border p-3">
|
|
<Label className="text-xs font-medium text-muted-foreground">테두리</Label>
|
|
<Switch
|
|
checked={(config as any).bordered || false}
|
|
onCheckedChange={(checked) => handleConfigChange("bordered", checked)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{/* meta-action 전용 */}
|
|
{type === "meta-action" && (
|
|
<>
|
|
<div className="space-y-3">
|
|
<div className="flex items-center gap-2">
|
|
<Label className="text-xs font-medium">버튼 스타일</Label>
|
|
</div>
|
|
<Separator className="mb-4" />
|
|
|
|
<div className="space-y-3">
|
|
<Label className="text-xs font-medium text-muted-foreground">버튼 크기</Label>
|
|
<Select
|
|
value={(config as any).buttonSize || "default"}
|
|
onValueChange={(v) => handleConfigChange("buttonSize", v)}
|
|
>
|
|
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="sm">Small</SelectItem>
|
|
<SelectItem value="default">Default</SelectItem>
|
|
<SelectItem value="lg">Large</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div className="space-y-3">
|
|
<Label className="text-xs font-medium text-muted-foreground">아이콘 (lucide 이름)</Label>
|
|
<Input
|
|
value={(config as any).icon || ""}
|
|
onChange={(e) => handleConfigChange("icon", e.target.value)}
|
|
placeholder="예: Save, Edit, Trash2"
|
|
className="h-8 text-xs sm:h-10 sm:text-sm"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
</TabsContent>
|
|
);
|
|
|
|
// 연동 탭
|
|
const renderBindingTab = () => {
|
|
const bindings = (config as any).bindings || [];
|
|
|
|
const addBinding = () => {
|
|
handleConfigChange("bindings", [
|
|
...bindings,
|
|
{
|
|
sourceComponentId: "",
|
|
sourceEvent: "change",
|
|
sourceField: "",
|
|
targetComponentId: "",
|
|
targetAction: "filter",
|
|
targetField: "",
|
|
},
|
|
]);
|
|
};
|
|
|
|
const removeBinding = (idx: number) => {
|
|
const newBindings = [...bindings];
|
|
newBindings.splice(idx, 1);
|
|
handleConfigChange("bindings", newBindings);
|
|
};
|
|
|
|
const updateBinding = (idx: number, key: string, value: any) => {
|
|
const newBindings = [...bindings];
|
|
newBindings[idx][key] = value;
|
|
handleConfigChange("bindings", newBindings);
|
|
};
|
|
|
|
return (
|
|
<TabsContent value="binding" className="space-y-4">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<Label className="text-xs font-medium">Reactive Binding</Label>
|
|
<p className="text-[10px] text-muted-foreground mt-1">
|
|
컴포넌트 간 이벤트-액션 연결 설정
|
|
</p>
|
|
</div>
|
|
<Button variant="outline" size="sm" onClick={addBinding}>
|
|
<Plus className="h-4 w-4 mr-1" />
|
|
바인딩 추가
|
|
</Button>
|
|
</div>
|
|
|
|
<Separator />
|
|
|
|
{bindings.length === 0 && (
|
|
<div className="flex flex-col items-center justify-center py-12 text-center">
|
|
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-muted">
|
|
<Link2 className="h-8 w-8 text-muted-foreground" />
|
|
</div>
|
|
<h4 className="mb-2 text-sm font-medium">바인딩이 없습니다</h4>
|
|
<p className="text-xs text-muted-foreground max-w-[250px]">
|
|
"바인딩 추가" 버튼을 클릭하여 컴포넌트 간 연결을 설정하세요.
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{bindings.map((binding: any, idx: number) => (
|
|
<div key={idx} className="space-y-3 rounded-lg border bg-muted/20 p-4">
|
|
<div className="flex items-center justify-between">
|
|
<Badge variant="outline" className="text-xs">바인딩 {idx + 1}</Badge>
|
|
<Button variant="ghost" size="sm" onClick={() => removeBinding(idx)}>
|
|
<Trash2 className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
|
|
{/* 소스 컴포넌트 */}
|
|
<div className="space-y-3">
|
|
<Label className="text-xs font-medium text-muted-foreground">소스 컴포넌트 ID</Label>
|
|
<Input
|
|
value={binding.sourceComponentId || ""}
|
|
onChange={(e) => updateBinding(idx, "sourceComponentId", e.target.value)}
|
|
placeholder="예: field-1"
|
|
className="h-8 text-xs sm:h-10 sm:text-sm"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-3">
|
|
<Label className="text-xs font-medium text-muted-foreground">소스 이벤트</Label>
|
|
<Select
|
|
value={binding.sourceEvent || "change"}
|
|
onValueChange={(v) => updateBinding(idx, "sourceEvent", v)}
|
|
>
|
|
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="change">change (값 변경)</SelectItem>
|
|
<SelectItem value="select">select (선택)</SelectItem>
|
|
<SelectItem value="click">click (클릭)</SelectItem>
|
|
<SelectItem value="load">load (로드)</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div className="space-y-3">
|
|
<Label className="text-xs font-medium text-muted-foreground">소스 필드 (선택)</Label>
|
|
<Input
|
|
value={binding.sourceField || ""}
|
|
onChange={(e) => updateBinding(idx, "sourceField", e.target.value)}
|
|
placeholder="예: customer_code"
|
|
className="h-8 text-xs sm:h-10 sm:text-sm"
|
|
/>
|
|
</div>
|
|
|
|
<Separator className="my-4" />
|
|
|
|
{/* 대상 컴포넌트 */}
|
|
<div className="space-y-3">
|
|
<Label className="text-xs font-medium text-muted-foreground">대상 컴포넌트 ID</Label>
|
|
<Input
|
|
value={binding.targetComponentId || ""}
|
|
onChange={(e) => updateBinding(idx, "targetComponentId", e.target.value)}
|
|
placeholder="예: dataview-1"
|
|
className="h-8 text-xs sm:h-10 sm:text-sm"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-3">
|
|
<Label className="text-xs font-medium text-muted-foreground">대상 액션</Label>
|
|
<Select
|
|
value={binding.targetAction || "filter"}
|
|
onValueChange={(v) => updateBinding(idx, "targetAction", v)}
|
|
>
|
|
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="filter">filter (필터링)</SelectItem>
|
|
<SelectItem value="setValue">setValue (값 설정)</SelectItem>
|
|
<SelectItem value="show">show (표시)</SelectItem>
|
|
<SelectItem value="hide">hide (숨김)</SelectItem>
|
|
<SelectItem value="enable">enable (활성화)</SelectItem>
|
|
<SelectItem value="disable">disable (비활성화)</SelectItem>
|
|
<SelectItem value="refresh">refresh (새로고침)</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div className="space-y-3">
|
|
<Label className="text-xs font-medium text-muted-foreground">대상 필드 (선택)</Label>
|
|
<Input
|
|
value={binding.targetField || ""}
|
|
onChange={(e) => updateBinding(idx, "targetField", e.target.value)}
|
|
placeholder="예: customer_code"
|
|
className="h-8 text-xs sm:h-10 sm:text-sm"
|
|
/>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</TabsContent>
|
|
);
|
|
};
|
|
|
|
// 조건 탭
|
|
const renderConditionTab = () => (
|
|
<TabsContent value="condition" className="space-y-4">
|
|
{/* 표시 조건 */}
|
|
<div className="space-y-3">
|
|
<div className="flex items-center gap-2">
|
|
<Label className="text-xs font-medium">표시 조건</Label>
|
|
</div>
|
|
<p className="text-[10px] text-muted-foreground">
|
|
특정 조건을 만족할 때만 컴포넌트 표시
|
|
</p>
|
|
<Separator className="my-3" />
|
|
|
|
<div className="space-y-3 rounded-lg border bg-muted/20 p-4">
|
|
<div className="space-y-3">
|
|
<Label className="text-xs font-medium text-muted-foreground">모드</Label>
|
|
<Select
|
|
value={(config as any).visibilityMode || "always"}
|
|
onValueChange={(v) => handleConfigChange("visibilityMode", v)}
|
|
>
|
|
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="always">항상 표시</SelectItem>
|
|
<SelectItem value="when_field_equals">필드 값 일치 시</SelectItem>
|
|
<SelectItem value="when_field_not_empty">필드 값 존재 시</SelectItem>
|
|
<SelectItem value="custom">커스텀 조건</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{["when_field_equals", "when_field_not_empty", "custom"].includes(
|
|
(config as any).visibilityMode
|
|
) && (
|
|
<>
|
|
<div className="space-y-3">
|
|
<Label className="text-xs font-medium text-muted-foreground">필드 ID</Label>
|
|
<Input
|
|
value={(config as any).visibilityFieldId || ""}
|
|
onChange={(e) => handleConfigChange("visibilityFieldId", e.target.value)}
|
|
placeholder="예: field-1"
|
|
className="h-8 text-xs sm:h-10 sm:text-sm"
|
|
/>
|
|
</div>
|
|
|
|
{(config as any).visibilityMode === "when_field_equals" && (
|
|
<div className="space-y-3">
|
|
<Label className="text-xs font-medium text-muted-foreground">비교 값</Label>
|
|
<Input
|
|
value={(config as any).visibilityValue || ""}
|
|
onChange={(e) => handleConfigChange("visibilityValue", e.target.value)}
|
|
placeholder="예: approved"
|
|
className="h-8 text-xs sm:h-10 sm:text-sm"
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{(config as any).visibilityMode === "custom" && (
|
|
<div className="space-y-3">
|
|
<Label className="text-xs font-medium text-muted-foreground">커스텀 표현식</Label>
|
|
<Input
|
|
value={(config as any).visibilityExpression || ""}
|
|
onChange={(e) => handleConfigChange("visibilityExpression", e.target.value)}
|
|
placeholder="예: field1 === 'value' && field2 > 10"
|
|
className="h-8 text-xs sm:h-10 sm:text-sm"
|
|
/>
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<Separator className="my-6" />
|
|
|
|
{/* 활성 조건 */}
|
|
<div className="space-y-3">
|
|
<div className="flex items-center gap-2">
|
|
<Label className="text-xs font-medium">활성 조건</Label>
|
|
</div>
|
|
<p className="text-[10px] text-muted-foreground">
|
|
특정 조건을 만족할 때만 컴포넌트 활성화 (입력/클릭 가능)
|
|
</p>
|
|
<Separator className="my-3" />
|
|
|
|
<div className="space-y-3 rounded-lg border bg-muted/20 p-4">
|
|
<div className="space-y-3">
|
|
<Label className="text-xs font-medium text-muted-foreground">모드</Label>
|
|
<Select
|
|
value={(config as any).enabledMode || "always"}
|
|
onValueChange={(v) => handleConfigChange("enabledMode", v)}
|
|
>
|
|
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="always">항상 활성</SelectItem>
|
|
<SelectItem value="when_field_equals">필드 값 일치 시</SelectItem>
|
|
<SelectItem value="when_field_not_empty">필드 값 존재 시</SelectItem>
|
|
<SelectItem value="custom">커스텀 조건</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{["when_field_equals", "when_field_not_empty", "custom"].includes(
|
|
(config as any).enabledMode
|
|
) && (
|
|
<>
|
|
<div className="space-y-3">
|
|
<Label className="text-xs font-medium text-muted-foreground">필드 ID</Label>
|
|
<Input
|
|
value={(config as any).enabledFieldId || ""}
|
|
onChange={(e) => handleConfigChange("enabledFieldId", e.target.value)}
|
|
placeholder="예: field-1"
|
|
className="h-8 text-xs sm:h-10 sm:text-sm"
|
|
/>
|
|
</div>
|
|
|
|
{(config as any).enabledMode === "when_field_equals" && (
|
|
<div className="space-y-3">
|
|
<Label className="text-xs font-medium text-muted-foreground">비교 값</Label>
|
|
<Input
|
|
value={(config as any).enabledValue || ""}
|
|
onChange={(e) => handleConfigChange("enabledValue", e.target.value)}
|
|
placeholder="예: edit"
|
|
className="h-8 text-xs sm:h-10 sm:text-sm"
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{(config as any).enabledMode === "custom" && (
|
|
<div className="space-y-3">
|
|
<Label className="text-xs font-medium text-muted-foreground">커스텀 표현식</Label>
|
|
<Input
|
|
value={(config as any).enabledExpression || ""}
|
|
onChange={(e) => handleConfigChange("enabledExpression", e.target.value)}
|
|
placeholder="예: mode === 'edit' && hasPermission"
|
|
className="h-8 text-xs sm:h-10 sm:text-sm"
|
|
/>
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</TabsContent>
|
|
);
|
|
|
|
return (
|
|
<div className={cn("flex h-full flex-col", className)}>
|
|
{/* 헤더 */}
|
|
<div className="flex items-center justify-between border-b bg-muted/30 p-4">
|
|
<div className="flex items-center gap-2">
|
|
<Settings2 className="h-5 w-5 text-primary" />
|
|
<h3 className="text-base font-semibold sm:text-lg">컴포넌트 설정</h3>
|
|
</div>
|
|
|
|
{/* 간편/상세 토글 (세그먼트 컨트롤 스타일) */}
|
|
<div className="inline-flex h-9 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground">
|
|
<button
|
|
onClick={() => setIsAdvanced(false)}
|
|
className={cn(
|
|
"inline-flex items-center justify-center gap-1.5 rounded-sm px-3 py-1.5 text-xs font-medium transition-all",
|
|
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
!isAdvanced
|
|
? "bg-background text-foreground shadow-sm"
|
|
: "hover:bg-background/50 hover:text-foreground"
|
|
)}
|
|
>
|
|
<Wand2 className="h-3.5 w-3.5" />
|
|
간편
|
|
</button>
|
|
<button
|
|
onClick={() => setIsAdvanced(true)}
|
|
className={cn(
|
|
"inline-flex items-center justify-center gap-1.5 rounded-sm px-3 py-1.5 text-xs font-medium transition-all",
|
|
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
isAdvanced
|
|
? "bg-background text-foreground shadow-sm"
|
|
: "hover:bg-background/50 hover:text-foreground"
|
|
)}
|
|
>
|
|
<SlidersHorizontal className="h-3.5 w-3.5" />
|
|
상세
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Tabs */}
|
|
<Tabs defaultValue="basic" className="flex-1 overflow-hidden">
|
|
<TabsList className="w-full justify-start border-b rounded-none bg-muted/20 px-4 h-auto">
|
|
<TabsTrigger
|
|
value="basic"
|
|
className="gap-1.5 data-[state=active]:border-b-2 data-[state=active]:border-primary rounded-none pb-3"
|
|
>
|
|
<Settings className="h-3.5 w-3.5" />
|
|
<span className="hidden sm:inline text-xs sm:text-sm">기본</span>
|
|
</TabsTrigger>
|
|
<TabsTrigger
|
|
value="data"
|
|
className="gap-1.5 data-[state=active]:border-b-2 data-[state=active]:border-primary rounded-none pb-3"
|
|
>
|
|
<Database className="h-3.5 w-3.5" />
|
|
<span className="hidden sm:inline text-xs sm:text-sm">데이터</span>
|
|
</TabsTrigger>
|
|
<TabsTrigger
|
|
value="display"
|
|
className="gap-1.5 data-[state=active]:border-b-2 data-[state=active]:border-primary rounded-none pb-3"
|
|
>
|
|
<Palette className="h-3.5 w-3.5" />
|
|
<span className="hidden sm:inline text-xs sm:text-sm">표시</span>
|
|
</TabsTrigger>
|
|
<TabsTrigger
|
|
value="binding"
|
|
className="gap-1.5 data-[state=active]:border-b-2 data-[state=active]:border-primary rounded-none pb-3"
|
|
>
|
|
<Link2 className="h-3.5 w-3.5" />
|
|
<span className="hidden sm:inline text-xs sm:text-sm">연동</span>
|
|
</TabsTrigger>
|
|
<TabsTrigger
|
|
value="condition"
|
|
className="gap-1.5 data-[state=active]:border-b-2 data-[state=active]:border-primary rounded-none pb-3"
|
|
>
|
|
<Eye className="h-3.5 w-3.5" />
|
|
<span className="hidden sm:inline text-xs sm:text-sm">조건</span>
|
|
</TabsTrigger>
|
|
</TabsList>
|
|
|
|
<div className="overflow-y-auto p-4">
|
|
{renderBasicTab()}
|
|
{renderDataTab()}
|
|
{renderDisplayTab()}
|
|
{renderBindingTab()}
|
|
{renderConditionTab()}
|
|
</div>
|
|
</Tabs>
|
|
</div>
|
|
);
|
|
}
|