Merge remote-tracking branch 'origin/main' into ksh

This commit is contained in:
SeongHyun Kim
2025-11-25 14:26:57 +09:00
62 changed files with 2521 additions and 1024 deletions

View File

@@ -470,7 +470,7 @@ function AppLayoutInner({ children }: AppLayoutProps) {
{/* 가운데 컨텐츠 영역 - 스크롤 가능 */}
<main className="h-[calc(100vh-3.5rem)] min-w-0 flex-1 overflow-auto bg-white">
<div className="h-full w-full p-4">{children}</div>
{children}
</main>
</div>

View File

@@ -346,6 +346,14 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
// 실제 사용 가능한 위젯 렌더링
const renderInteractiveWidget = (comp: ComponentData) => {
console.log("🎯 renderInteractiveWidget 호출:", {
type: comp.type,
id: comp.id,
componentId: (comp as any).componentId,
hasComponentConfig: !!(comp as any).componentConfig,
componentConfig: (comp as any).componentConfig,
});
// 데이터 테이블 컴포넌트 처리
if (comp.type === "datatable") {
return (
@@ -397,6 +405,39 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
);
}
// 탭 컴포넌트 처리
const componentType = (comp as any).componentType || (comp as any).componentId;
if (comp.type === "tabs" || (comp.type === "component" && componentType === "tabs-widget")) {
const TabsWidget = require("@/components/screen/widgets/TabsWidget").TabsWidget;
// componentConfig에서 탭 정보 추출
const tabsConfig = comp.componentConfig || {};
const tabsComponent = {
...comp,
type: "tabs" as const,
tabs: tabsConfig.tabs || [],
defaultTab: tabsConfig.defaultTab,
orientation: tabsConfig.orientation || "horizontal",
variant: tabsConfig.variant || "default",
allowCloseable: tabsConfig.allowCloseable || false,
persistSelection: tabsConfig.persistSelection || false,
};
console.log("🔍 탭 컴포넌트 렌더링:", {
originalType: comp.type,
componentType,
componentId: (comp as any).componentId,
tabs: tabsComponent.tabs,
tabsConfig,
});
return (
<div className="h-full w-full">
<TabsWidget component={tabsComponent as any} />
</div>
);
}
const { widgetType, label, placeholder, required, readonly, columnName } = comp;
const fieldName = columnName || comp.id;
const currentValue = formData[fieldName] || "";

View File

@@ -554,6 +554,73 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
);
})()}
{/* 탭 컴포넌트 타입 */}
{(type === "tabs" || (type === "component" && ((component as any).componentType === "tabs-widget" || (component as any).componentId === "tabs-widget"))) &&
(() => {
console.log("🎯 탭 컴포넌트 조건 충족:", {
type,
componentType: (component as any).componentType,
componentId: (component as any).componentId,
isDesignMode,
});
if (isDesignMode) {
// 디자인 모드: 미리보기 표시
const tabsComponent = component as any;
const tabs = tabsComponent.componentConfig?.tabs || tabsComponent.tabs || [];
return (
<div className="flex h-full w-full items-center justify-center rounded border-2 border-dashed border-gray-300 bg-gray-50">
<div className="text-center">
<div className="flex items-center justify-center">
<Folder className="h-8 w-8 text-gray-400" />
</div>
<p className="text-muted-foreground mt-2 text-sm font-medium"> </p>
<p className="text-xs text-gray-400">
{tabs.length > 0
? `${tabs.length}개의 탭 (실제 화면에서 표시됩니다)`
: "탭이 없습니다. 설정 패널에서 탭을 추가하세요"}
</p>
{tabs.length > 0 && (
<div className="mt-2 flex flex-wrap justify-center gap-1">
{tabs.map((tab: any, index: number) => (
<Badge key={tab.id} variant="outline" className="text-xs">
{tab.label || `${index + 1}`}
{tab.screenName && (
<span className="ml-1 text-[10px] text-gray-400">
({tab.screenName})
</span>
)}
</Badge>
))}
</div>
)}
</div>
</div>
);
} else {
// 실제 화면: TabsWidget 렌더링
const TabsWidget = require("@/components/screen/widgets/TabsWidget").TabsWidget;
const tabsConfig = (component as any).componentConfig || {};
const tabsComponent = {
...component,
type: "tabs" as const,
tabs: tabsConfig.tabs || [],
defaultTab: tabsConfig.defaultTab,
orientation: tabsConfig.orientation || "horizontal",
variant: tabsConfig.variant || "default",
allowCloseable: tabsConfig.allowCloseable || false,
persistSelection: tabsConfig.persistSelection || false,
};
return (
<div className="h-full w-full">
<TabsWidget component={tabsComponent as any} />
</div>
);
}
})()}
{/* 그룹 타입 */}
{type === "group" && (
<div className="relative h-full w-full">

View File

@@ -35,9 +35,9 @@ export const ResponsiveDesignerContainer: React.FC<ResponsiveDesignerContainerPr
switch (viewMode) {
case "fit":
// 컨테이너에 맞춰 비율 유지하며 조정 (여백 허용)
const scaleX = (containerSize.width - 40) / designWidth;
const scaleY = (containerSize.height - 40) / designHeight;
// 컨테이너에 맞춰 비율 유지하며 조정 (좌우 여백 16px씩 유지)
const scaleX = (containerSize.width - 32) / designWidth;
const scaleY = (containerSize.height - 64) / designHeight;
return Math.min(scaleX, scaleY, 2); // 최대 2배까지 허용
case "custom":
@@ -154,7 +154,7 @@ export const ResponsiveDesignerContainer: React.FC<ResponsiveDesignerContainerPr
{/* 디자인 영역 */}
<div
ref={containerRef}
className="flex-1 overflow-auto p-8"
className="flex-1 overflow-auto px-4 py-8"
style={{
justifyContent: "center",
alignItems: "flex-start",

View File

@@ -981,7 +981,18 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
// 스페이스바 키 이벤트 처리 (Pan 모드) + 전역 마우스 이벤트
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
// 입력 필드에서는 스페이스바 무시
// 입력 필드에서는 스페이스바 무시 (activeElement로 정확하게 체크)
const activeElement = document.activeElement;
if (
activeElement instanceof HTMLInputElement ||
activeElement instanceof HTMLTextAreaElement ||
activeElement?.getAttribute('contenteditable') === 'true' ||
activeElement?.getAttribute('role') === 'textbox'
) {
return;
}
// e.target도 함께 체크 (이중 방어)
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
return;
}
@@ -997,6 +1008,17 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
};
const handleKeyUp = (e: KeyboardEvent) => {
// 입력 필드에서는 스페이스바 무시
const activeElement = document.activeElement;
if (
activeElement instanceof HTMLInputElement ||
activeElement instanceof HTMLTextAreaElement ||
activeElement?.getAttribute('contenteditable') === 'true' ||
activeElement?.getAttribute('role') === 'textbox'
) {
return;
}
if (e.code === "Space") {
e.preventDefault(); // 스페이스바 기본 스크롤 동작 차단
setIsPanMode(false);

View File

@@ -35,7 +35,10 @@ import {
DialogDescription,
} from "@/components/ui/dialog";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { MoreHorizontal, Edit, Trash2, Copy, Eye, Plus, Search, Palette, RotateCcw, Trash } from "lucide-react";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
import { cn } from "@/lib/utils";
import { MoreHorizontal, Edit, Trash2, Copy, Eye, Plus, Search, Palette, RotateCcw, Trash, Check, ChevronsUpDown } from "lucide-react";
import { ScreenDefinition } from "@/types/screen";
import { screenApi } from "@/lib/api/screen";
import CreateScreenModal from "./CreateScreenModal";
@@ -127,8 +130,9 @@ export default function ScreenList({ onScreenSelect, selectedScreen, onDesignScr
isActive: "Y",
tableName: "",
});
const [tables, setTables] = useState<string[]>([]);
const [tables, setTables] = useState<Array<{ tableName: string; tableLabel: string }>>([]);
const [loadingTables, setLoadingTables] = useState(false);
const [tableComboboxOpen, setTableComboboxOpen] = useState(false);
// 미리보기 관련 상태
const [previewDialogOpen, setPreviewDialogOpen] = useState(false);
@@ -279,9 +283,12 @@ export default function ScreenList({ onScreenSelect, selectedScreen, onDesignScr
const { tableManagementApi } = await import("@/lib/api/tableManagement");
const response = await tableManagementApi.getTableList();
if (response.success && response.data) {
// tableName만 추출 (camelCase)
const tableNames = response.data.map((table: any) => table.tableName);
setTables(tableNames);
// tableName과 displayName 매핑 (백엔드에서 displayName으로 라벨을 반환함)
const tableList = response.data.map((table: any) => ({
tableName: table.tableName,
tableLabel: table.displayName || table.tableName,
}));
setTables(tableList);
}
} catch (error) {
console.error("테이블 목록 조회 실패:", error);
@@ -297,6 +304,10 @@ export default function ScreenList({ onScreenSelect, selectedScreen, onDesignScr
// 화면 정보 업데이트 API 호출
await screenApi.updateScreenInfo(screenToEdit.screenId, editFormData);
// 선택된 테이블의 라벨 찾기
const selectedTable = tables.find((t) => t.tableName === editFormData.tableName);
const tableLabel = selectedTable?.tableLabel || editFormData.tableName;
// 목록에서 해당 화면 정보 업데이트
setScreens((prev) =>
prev.map((s) =>
@@ -304,6 +315,8 @@ export default function ScreenList({ onScreenSelect, selectedScreen, onDesignScr
? {
...s,
screenName: editFormData.screenName,
tableName: editFormData.tableName,
tableLabel: tableLabel,
description: editFormData.description,
isActive: editFormData.isActive,
}
@@ -1202,22 +1215,62 @@ export default function ScreenList({ onScreenSelect, selectedScreen, onDesignScr
</div>
<div className="space-y-2">
<Label htmlFor="edit-tableName"> *</Label>
<Select
value={editFormData.tableName}
onValueChange={(value) => setEditFormData({ ...editFormData, tableName: value })}
disabled={loadingTables}
>
<SelectTrigger id="edit-tableName">
<SelectValue placeholder={loadingTables ? "로딩 중..." : "테이블을 선택하세요"} />
</SelectTrigger>
<SelectContent>
{tables.map((tableName) => (
<SelectItem key={tableName} value={tableName}>
{tableName}
</SelectItem>
))}
</SelectContent>
</Select>
<Popover open={tableComboboxOpen} onOpenChange={setTableComboboxOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={tableComboboxOpen}
className="h-8 w-full justify-between text-xs sm:h-10 sm:text-sm"
disabled={loadingTables}
>
{loadingTables
? "로딩 중..."
: editFormData.tableName
? tables.find((table) => table.tableName === editFormData.tableName)?.tableLabel || editFormData.tableName
: "테이블을 선택하세요"}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent
className="p-0"
style={{ width: "var(--radix-popover-trigger-width)" }}
align="start"
>
<Command>
<CommandInput placeholder="테이블 검색..." className="text-xs sm:text-sm" />
<CommandList>
<CommandEmpty className="text-xs sm:text-sm">
.
</CommandEmpty>
<CommandGroup>
{tables.map((table) => (
<CommandItem
key={table.tableName}
value={`${table.tableName} ${table.tableLabel}`}
onSelect={() => {
setEditFormData({ ...editFormData, tableName: table.tableName });
setTableComboboxOpen(false);
}}
className="text-xs sm:text-sm"
>
<Check
className={cn(
"mr-2 h-4 w-4",
editFormData.tableName === table.tableName ? "opacity-100" : "opacity-0"
)}
/>
<div className="flex flex-col">
<span className="font-medium">{table.tableLabel}</span>
<span className="text-[10px] text-gray-500">{table.tableName}</span>
</div>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</div>
<div className="space-y-2">
<Label htmlFor="edit-description"></Label>

View File

@@ -49,7 +49,6 @@ export default function StyleEditor({ style, onStyleChange, className }: StyleEd
value={localStyle.borderWidth || ""}
onChange={(e) => handleStyleChange("borderWidth", e.target.value)}
className="h-6 w-full px-2 py-0 text-xs"
style={{ fontSize: "12px" }}
/>
</div>
<div className="space-y-1">
@@ -60,20 +59,20 @@ export default function StyleEditor({ style, onStyleChange, className }: StyleEd
value={localStyle.borderStyle || "solid"}
onValueChange={(value) => handleStyleChange("borderStyle", value)}
>
<SelectTrigger className="h-6 w-full px-2 py-0" style={{ fontSize: "12px" }}>
<SelectTrigger className=" text-xsh-6 w-full px-2 py-0">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="solid" style={{ fontSize: "12px" }}>
<SelectItem value="solid" className="text-xs">
</SelectItem>
<SelectItem value="dashed" style={{ fontSize: "12px" }}>
<SelectItem value="dashed" className="text-xs">
</SelectItem>
<SelectItem value="dotted" style={{ fontSize: "12px" }}>
<SelectItem value="dotted" className="text-xs">
</SelectItem>
<SelectItem value="none" style={{ fontSize: "12px" }}>
<SelectItem value="none" className="text-xs">
</SelectItem>
</SelectContent>
@@ -93,7 +92,7 @@ export default function StyleEditor({ style, onStyleChange, className }: StyleEd
value={localStyle.borderColor || "#000000"}
onChange={(e) => handleStyleChange("borderColor", e.target.value)}
className="h-6 w-12 p-1"
style={{ fontSize: "12px" }}
className="text-xs"
/>
<Input
type="text"
@@ -101,7 +100,6 @@ export default function StyleEditor({ style, onStyleChange, className }: StyleEd
onChange={(e) => handleStyleChange("borderColor", e.target.value)}
placeholder="#000000"
className="h-6 flex-1 text-xs"
style={{ fontSize: "12px" }}
/>
</div>
</div>
@@ -116,7 +114,6 @@ export default function StyleEditor({ style, onStyleChange, className }: StyleEd
value={localStyle.borderRadius || ""}
onChange={(e) => handleStyleChange("borderRadius", e.target.value)}
className="h-6 w-full px-2 py-0 text-xs"
style={{ fontSize: "12px" }}
/>
</div>
</div>
@@ -142,7 +139,7 @@ export default function StyleEditor({ style, onStyleChange, className }: StyleEd
value={localStyle.backgroundColor || "#ffffff"}
onChange={(e) => handleStyleChange("backgroundColor", e.target.value)}
className="h-6 w-12 p-1"
style={{ fontSize: "12px" }}
className="text-xs"
/>
<Input
type="text"
@@ -150,7 +147,6 @@ export default function StyleEditor({ style, onStyleChange, className }: StyleEd
onChange={(e) => handleStyleChange("backgroundColor", e.target.value)}
placeholder="#ffffff"
className="h-6 flex-1 text-xs"
style={{ fontSize: "12px" }}
/>
</div>
</div>
@@ -166,7 +162,6 @@ export default function StyleEditor({ style, onStyleChange, className }: StyleEd
value={localStyle.backgroundImage || ""}
onChange={(e) => handleStyleChange("backgroundImage", e.target.value)}
className="h-6 w-full px-2 py-0 text-xs"
style={{ fontSize: "12px" }}
/>
<p className="text-[10px] text-muted-foreground">
( )
@@ -195,7 +190,7 @@ export default function StyleEditor({ style, onStyleChange, className }: StyleEd
value={localStyle.color || "#000000"}
onChange={(e) => handleStyleChange("color", e.target.value)}
className="h-6 w-12 p-1"
style={{ fontSize: "12px" }}
className="text-xs"
/>
<Input
type="text"
@@ -203,7 +198,6 @@ export default function StyleEditor({ style, onStyleChange, className }: StyleEd
onChange={(e) => handleStyleChange("color", e.target.value)}
placeholder="#000000"
className="h-6 flex-1 text-xs"
style={{ fontSize: "12px" }}
/>
</div>
</div>
@@ -218,7 +212,6 @@ export default function StyleEditor({ style, onStyleChange, className }: StyleEd
value={localStyle.fontSize || ""}
onChange={(e) => handleStyleChange("fontSize", e.target.value)}
className="h-6 w-full px-2 py-0 text-xs"
style={{ fontSize: "12px" }}
/>
</div>
</div>
@@ -232,29 +225,29 @@ export default function StyleEditor({ style, onStyleChange, className }: StyleEd
value={localStyle.fontWeight || "normal"}
onValueChange={(value) => handleStyleChange("fontWeight", value)}
>
<SelectTrigger className="h-6 w-full px-2 py-0" style={{ fontSize: "12px" }}>
<SelectTrigger className=" text-xsh-6 w-full px-2 py-0">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="normal" style={{ fontSize: "12px" }}>
<SelectItem value="normal" className="text-xs">
</SelectItem>
<SelectItem value="bold" style={{ fontSize: "12px" }}>
<SelectItem value="bold" className="text-xs">
</SelectItem>
<SelectItem value="100" style={{ fontSize: "12px" }}>
<SelectItem value="100" className="text-xs">
100
</SelectItem>
<SelectItem value="400" style={{ fontSize: "12px" }}>
<SelectItem value="400" className="text-xs">
400
</SelectItem>
<SelectItem value="500" style={{ fontSize: "12px" }}>
<SelectItem value="500" className="text-xs">
500
</SelectItem>
<SelectItem value="600" style={{ fontSize: "12px" }}>
<SelectItem value="600" className="text-xs">
600
</SelectItem>
<SelectItem value="700" style={{ fontSize: "12px" }}>
<SelectItem value="700" className="text-xs">
700
</SelectItem>
</SelectContent>
@@ -268,20 +261,20 @@ export default function StyleEditor({ style, onStyleChange, className }: StyleEd
value={localStyle.textAlign || "left"}
onValueChange={(value) => handleStyleChange("textAlign", value)}
>
<SelectTrigger className="h-6 w-full px-2 py-0" style={{ fontSize: "12px" }}>
<SelectTrigger className=" text-xsh-6 w-full px-2 py-0">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="left" style={{ fontSize: "12px" }}>
<SelectItem value="left" className="text-xs">
</SelectItem>
<SelectItem value="center" style={{ fontSize: "12px" }}>
<SelectItem value="center" className="text-xs">
</SelectItem>
<SelectItem value="right" style={{ fontSize: "12px" }}>
<SelectItem value="right" className="text-xs">
</SelectItem>
<SelectItem value="justify" style={{ fontSize: "12px" }}>
<SelectItem value="justify" className="text-xs">
</SelectItem>
</SelectContent>

View File

@@ -509,7 +509,7 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
role="combobox"
aria-expanded={modalScreenOpen}
className="h-6 w-full justify-between px-2 py-0"
style={{ fontSize: "12px" }}
className="text-xs"
disabled={screensLoading}
>
{config.action?.targetScreenId
@@ -900,7 +900,7 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
role="combobox"
aria-expanded={modalScreenOpen}
className="h-6 w-full justify-between px-2 py-0"
style={{ fontSize: "12px" }}
className="text-xs"
disabled={screensLoading}
>
{config.action?.targetScreenId
@@ -978,7 +978,7 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
role="combobox"
aria-expanded={modalScreenOpen}
className="h-6 w-full justify-between px-2 py-0"
style={{ fontSize: "12px" }}
className="text-xs"
disabled={screensLoading}
>
{config.action?.targetScreenId
@@ -1132,7 +1132,7 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
role="combobox"
aria-expanded={modalScreenOpen}
className="h-6 w-full justify-between px-2 py-0"
style={{ fontSize: "12px" }}
className="text-xs"
disabled={screensLoading}
>
{config.action?.targetScreenId
@@ -1286,7 +1286,6 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
role="combobox"
aria-expanded={displayColumnOpen}
className="mt-2 h-8 w-full justify-between text-xs"
style={{ fontSize: "12px" }}
disabled={columnsLoading || tableColumns.length === 0}
>
{columnsLoading
@@ -1301,9 +1300,9 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
</PopoverTrigger>
<PopoverContent className="p-0" style={{ width: "var(--radix-popover-trigger-width)" }} align="start">
<Command>
<CommandInput placeholder="컬럼 검색..." className="text-xs" style={{ fontSize: "12px" }} />
<CommandInput placeholder="컬럼 검색..." className="text-xs" />
<CommandList>
<CommandEmpty className="text-xs" style={{ fontSize: "12px" }}>
<CommandEmpty className="text-xs">
.
</CommandEmpty>
<CommandGroup>
@@ -1316,7 +1315,6 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
setDisplayColumnOpen(false);
}}
className="text-xs"
style={{ fontSize: "12px" }}
>
<Check
className={cn(
@@ -1350,7 +1348,7 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
role="combobox"
aria-expanded={navScreenOpen}
className="h-6 w-full justify-between px-2 py-0"
style={{ fontSize: "12px" }}
className="text-xs"
disabled={screensLoading}
>
{config.action?.targetScreenId
@@ -1424,7 +1422,6 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
onUpdateProperty("componentConfig.action.targetUrl", newValue);
}}
className="h-6 w-full px-2 py-0 text-xs"
style={{ fontSize: "12px" }}
/>
<p className="mt-1 text-xs text-muted-foreground">URL을 </p>
</div>

View File

@@ -117,7 +117,7 @@ export const CheckboxConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-xs" style={{ fontSize: "12px" }}>
<CardTitle className="flex items-center gap-2 text-xs">
<CheckSquare className="h-4 w-4" />
</CardTitle>
@@ -173,7 +173,7 @@ export const CheckboxConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
value={localConfig.label || ""}
onChange={(e) => updateConfig("label", e.target.value)}
placeholder="체크박스 라벨"
className="text-xs" style={{ fontSize: "12px" }}
className="text-xs"
/>
</div>
@@ -187,7 +187,7 @@ export const CheckboxConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
value={localConfig.checkedValue || ""}
onChange={(e) => updateConfig("checkedValue", e.target.value)}
placeholder="Y"
className="text-xs" style={{ fontSize: "12px" }}
className="text-xs"
/>
</div>
<div className="space-y-2">
@@ -199,7 +199,7 @@ export const CheckboxConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
value={localConfig.uncheckedValue || ""}
onChange={(e) => updateConfig("uncheckedValue", e.target.value)}
placeholder="N"
className="text-xs" style={{ fontSize: "12px" }}
className="text-xs"
/>
</div>
</div>
@@ -232,7 +232,7 @@ export const CheckboxConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
value={localConfig.groupLabel || ""}
onChange={(e) => updateConfig("groupLabel", e.target.value)}
placeholder="체크박스 그룹 제목"
className="text-xs" style={{ fontSize: "12px" }}
className="text-xs"
/>
</div>
@@ -244,19 +244,19 @@ export const CheckboxConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
value={newOptionLabel}
onChange={(e) => setNewOptionLabel(e.target.value)}
placeholder="라벨"
className="flex-1 text-xs" style={{ fontSize: "12px" }}
className="flex-1 text-xs"
/>
<Input
value={newOptionValue}
onChange={(e) => setNewOptionValue(e.target.value)}
placeholder="값"
className="flex-1 text-xs" style={{ fontSize: "12px" }}
className="flex-1 text-xs"
/>
<Button
size="sm"
onClick={addOption}
disabled={!newOptionLabel.trim() || !newOptionValue.trim()}
className="text-xs" style={{ fontSize: "12px" }}
className="text-xs"
>
<Plus className="h-3 w-3" />
</Button>
@@ -277,13 +277,13 @@ export const CheckboxConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
value={option.label}
onChange={(e) => updateOption(index, "label", e.target.value)}
placeholder="라벨"
className="flex-1 text-xs" style={{ fontSize: "12px" }}
className="flex-1 text-xs"
/>
<Input
value={option.value}
onChange={(e) => updateOption(index, "value", e.target.value)}
placeholder="값"
className="flex-1 text-xs" style={{ fontSize: "12px" }}
className="flex-1 text-xs"
/>
<Switch
checked={!option.disabled}
@@ -361,7 +361,7 @@ export const CheckboxConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
disabled={localConfig.readonly}
required={localConfig.required}
defaultChecked={localConfig.defaultChecked}
className="text-xs" style={{ fontSize: "12px" }}
className="text-xs"
/>
<Label htmlFor="preview-single" className="text-xs">
{localConfig.label || "체크박스 라벨"}
@@ -380,7 +380,7 @@ export const CheckboxConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
disabled={localConfig.readonly || option.disabled}
required={localConfig.required && index === 0} // 첫 번째에만 required 표시
defaultChecked={option.checked}
className="text-xs" style={{ fontSize: "12px" }}
className="text-xs"
/>
<Label htmlFor={`preview-group-${index}`} className="text-xs">
{option.label}

View File

@@ -106,7 +106,7 @@ export const CodeConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-xs" style={{ fontSize: "12px" }}>
<CardTitle className="flex items-center gap-2 text-xs">
<Code className="h-4 w-4" />
</CardTitle>
@@ -174,7 +174,7 @@ export const CodeConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
step={50}
value={localConfig.height || 300}
onChange={(e) => updateConfig("height", parseInt(e.target.value))}
className="text-xs" style={{ fontSize: "12px" }}
className="text-xs"
/>
<div className="text-muted-foreground flex justify-between text-xs">
<span>150px</span>
@@ -199,7 +199,7 @@ export const CodeConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
onChange={(e) => updateConfig("fontSize", parseInt(e.target.value))}
min={10}
max={24}
className="text-xs" style={{ fontSize: "12px" }}
className="text-xs"
/>
</div>
@@ -214,7 +214,7 @@ export const CodeConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
onChange={(e) => updateConfig("tabSize", parseInt(e.target.value))}
min={1}
max={8}
className="text-xs" style={{ fontSize: "12px" }}
className="text-xs"
/>
</div>
</div>
@@ -308,7 +308,7 @@ export const CodeConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
value={localConfig.placeholder || ""}
onChange={(e) => updateConfig("placeholder", e.target.value)}
placeholder="코드를 입력하세요..."
className="text-xs" style={{ fontSize: "12px" }}
className="text-xs"
/>
</div>
@@ -330,7 +330,7 @@ export const CodeConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
value={localConfig.defaultValue || ""}
onChange={(e) => updateConfig("defaultValue", e.target.value)}
placeholder="기본 코드 내용"
className="font-mono text-xs" style={{ fontSize: "12px" }}
className="font-mono text-xs"
rows={4}
/>
</div>

View File

@@ -75,7 +75,7 @@ export const DateConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-xs" style={{ fontSize: "12px" }}>
<CardTitle className="flex items-center gap-2 text-xs">
<Calendar className="h-4 w-4" />
</CardTitle>
@@ -95,7 +95,7 @@ export const DateConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
value={localConfig.placeholder || ""}
onChange={(e) => updateConfig("placeholder", e.target.value)}
placeholder="날짜를 선택하세요"
className="text-xs" style={{ fontSize: "12px" }}
className="text-xs"
/>
</div>
@@ -149,7 +149,7 @@ export const DateConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
type={localConfig.showTime ? "datetime-local" : "date"}
value={localConfig.minDate || ""}
onChange={(e) => updateConfig("minDate", e.target.value)}
className="flex-1 text-xs" style={{ fontSize: "12px" }}
className="flex-1 text-xs"
/>
<Button size="sm" variant="outline" onClick={() => setCurrentDate("minDate")} className="text-xs">
@@ -167,7 +167,7 @@ export const DateConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
type={localConfig.showTime ? "datetime-local" : "date"}
value={localConfig.maxDate || ""}
onChange={(e) => updateConfig("maxDate", e.target.value)}
className="flex-1 text-xs" style={{ fontSize: "12px" }}
className="flex-1 text-xs"
/>
<Button size="sm" variant="outline" onClick={() => setCurrentDate("maxDate")} className="text-xs">
@@ -190,7 +190,7 @@ export const DateConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
type={localConfig.showTime ? "datetime-local" : "date"}
value={localConfig.defaultValue || ""}
onChange={(e) => updateConfig("defaultValue", e.target.value)}
className="flex-1 text-xs" style={{ fontSize: "12px" }}
className="flex-1 text-xs"
/>
<Button size="sm" variant="outline" onClick={() => setCurrentDate("defaultValue")} className="text-xs">
@@ -245,7 +245,7 @@ export const DateConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
min={localConfig.minDate}
max={localConfig.maxDate}
defaultValue={localConfig.defaultValue}
className="text-xs" style={{ fontSize: "12px" }}
className="text-xs"
/>
<div className="text-muted-foreground mt-2 text-xs">
: {localConfig.format}

View File

@@ -51,37 +51,52 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
const [newFieldName, setNewFieldName] = useState("");
const [newFieldLabel, setNewFieldLabel] = useState("");
const [newFieldType, setNewFieldType] = useState("string");
const [isUserEditing, setIsUserEditing] = useState(false);
// 컴포넌트 변경 시 로컬 상태 동기화
// 컴포넌트 변경 시 로컬 상태 동기화 (사용자가 입력 중이 아닐 때만)
useEffect(() => {
const currentConfig = (widget.webTypeConfig as EntityTypeConfig) || {};
setLocalConfig({
entityType: currentConfig.entityType || "",
displayFields: currentConfig.displayFields || [],
searchFields: currentConfig.searchFields || [],
valueField: currentConfig.valueField || "id",
labelField: currentConfig.labelField || "name",
multiple: currentConfig.multiple || false,
searchable: currentConfig.searchable !== false,
placeholder: currentConfig.placeholder || "엔티티를 선택하세요",
emptyMessage: currentConfig.emptyMessage || "검색 결과가 없습니다",
pageSize: currentConfig.pageSize || 20,
minSearchLength: currentConfig.minSearchLength || 1,
defaultValue: currentConfig.defaultValue || "",
required: currentConfig.required || false,
readonly: currentConfig.readonly || false,
apiEndpoint: currentConfig.apiEndpoint || "",
filters: currentConfig.filters || {},
});
}, [widget.webTypeConfig]);
if (!isUserEditing) {
const currentConfig = (widget.webTypeConfig as EntityTypeConfig) || {};
setLocalConfig({
entityType: currentConfig.entityType || "",
displayFields: currentConfig.displayFields || [],
searchFields: currentConfig.searchFields || [],
valueField: currentConfig.valueField || "id",
labelField: currentConfig.labelField || "name",
multiple: currentConfig.multiple || false,
searchable: currentConfig.searchable !== false,
placeholder: currentConfig.placeholder || "엔티티를 선택하세요",
emptyMessage: currentConfig.emptyMessage || "검색 결과가 없습니다",
pageSize: currentConfig.pageSize || 20,
minSearchLength: currentConfig.minSearchLength || 1,
defaultValue: currentConfig.defaultValue || "",
required: currentConfig.required || false,
readonly: currentConfig.readonly || false,
apiEndpoint: currentConfig.apiEndpoint || "",
filters: currentConfig.filters || {},
});
}
}, [widget.webTypeConfig, isUserEditing]);
// 설정 업데이트 핸들러
// 설정 업데이트 핸들러 (즉시 부모에게 전달 - 드롭다운, 체크박스 등)
const updateConfig = (field: keyof EntityTypeConfig, value: any) => {
const newConfig = { ...localConfig, [field]: value };
setLocalConfig(newConfig);
onUpdateProperty("webTypeConfig", newConfig);
};
// 입력 필드용 업데이트 (로컬 상태만)
const updateConfigLocal = (field: keyof EntityTypeConfig, value: any) => {
setIsUserEditing(true);
setLocalConfig({ ...localConfig, [field]: value });
};
// 입력 완료 시 부모에게 전달
const handleInputBlur = () => {
setIsUserEditing(false);
onUpdateProperty("webTypeConfig", localConfig);
};
// 필드 추가
const addDisplayField = () => {
if (!newFieldName.trim() || !newFieldLabel.trim()) return;
@@ -106,11 +121,18 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
updateConfig("displayFields", newFields);
};
// 필드 업데이트
// 필드 업데이트 (입력 중)
const updateDisplayField = (index: number, field: keyof EntityField, value: any) => {
setIsUserEditing(true);
const newFields = [...localConfig.displayFields];
newFields[index] = { ...newFields[index], [field]: value };
updateConfig("displayFields", newFields);
setLocalConfig({ ...localConfig, displayFields: newFields });
};
// 필드 업데이트 완료 (onBlur)
const handleFieldBlur = () => {
setIsUserEditing(false);
onUpdateProperty("webTypeConfig", localConfig);
};
// 검색 필드 토글
@@ -163,7 +185,7 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-xs" style={{ fontSize: "12px" }}>
<CardTitle className="flex items-center gap-2 text-xs">
<Database className="h-4 w-4" />
</CardTitle>
@@ -181,9 +203,10 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
<Input
id="entityType"
value={localConfig.entityType || ""}
onChange={(e) => updateConfig("entityType", e.target.value)}
onChange={(e) => updateConfigLocal("entityType", e.target.value)}
onBlur={handleInputBlur}
placeholder="user, product, department..."
className="text-xs" style={{ fontSize: "12px" }}
className="text-xs"
/>
</div>
@@ -196,7 +219,7 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
size="sm"
variant="outline"
onClick={() => applyEntityType(entity.value)}
className="text-xs" style={{ fontSize: "12px" }}
className="text-xs"
>
{entity.label}
</Button>
@@ -211,9 +234,10 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
<Input
id="apiEndpoint"
value={localConfig.apiEndpoint || ""}
onChange={(e) => updateConfig("apiEndpoint", e.target.value)}
onChange={(e) => updateConfigLocal("apiEndpoint", e.target.value)}
onBlur={handleInputBlur}
placeholder="/api/entities/user"
className="text-xs" style={{ fontSize: "12px" }}
className="text-xs"
/>
</div>
</div>
@@ -230,9 +254,10 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
<Input
id="valueField"
value={localConfig.valueField || ""}
onChange={(e) => updateConfig("valueField", e.target.value)}
onChange={(e) => updateConfigLocal("valueField", e.target.value)}
onBlur={handleInputBlur}
placeholder="id"
className="text-xs" style={{ fontSize: "12px" }}
className="text-xs"
/>
</div>
@@ -243,9 +268,10 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
<Input
id="labelField"
value={localConfig.labelField || ""}
onChange={(e) => updateConfig("labelField", e.target.value)}
onChange={(e) => updateConfigLocal("labelField", e.target.value)}
onBlur={handleInputBlur}
placeholder="name"
className="text-xs" style={{ fontSize: "12px" }}
className="text-xs"
/>
</div>
</div>
@@ -263,13 +289,13 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
value={newFieldName}
onChange={(e) => setNewFieldName(e.target.value)}
placeholder="필드명"
className="flex-1 text-xs" style={{ fontSize: "12px" }}
className="flex-1 text-xs"
/>
<Input
value={newFieldLabel}
onChange={(e) => setNewFieldLabel(e.target.value)}
placeholder="라벨"
className="flex-1 text-xs" style={{ fontSize: "12px" }}
className="flex-1 text-xs"
/>
<Select value={newFieldType} onValueChange={setNewFieldType}>
<SelectTrigger className="w-24 text-xs">
@@ -287,7 +313,7 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
size="sm"
onClick={addDisplayField}
disabled={!newFieldName.trim() || !newFieldLabel.trim()}
className="text-xs" style={{ fontSize: "12px" }}
className="text-xs"
>
<Plus className="h-3 w-3" />
</Button>
@@ -302,19 +328,24 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
<div key={index} className="flex items-center gap-2 rounded border p-2">
<Switch
checked={field.visible}
onCheckedChange={(checked) => updateDisplayField(index, "visible", checked)}
onCheckedChange={(checked) => {
updateDisplayField(index, "visible", checked);
handleFieldBlur();
}}
/>
<Input
value={field.name}
onChange={(e) => updateDisplayField(index, "name", e.target.value)}
onBlur={handleFieldBlur}
placeholder="필드명"
className="flex-1 text-xs" style={{ fontSize: "12px" }}
className="flex-1 text-xs"
/>
<Input
value={field.label}
onChange={(e) => updateDisplayField(index, "label", e.target.value)}
onBlur={handleFieldBlur}
placeholder="라벨"
className="flex-1 text-xs" style={{ fontSize: "12px" }}
className="flex-1 text-xs"
/>
<Select value={field.type} onValueChange={(value) => updateDisplayField(index, "type", value)}>
<SelectTrigger className="w-24 text-xs">
@@ -332,7 +363,7 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
size="sm"
variant={localConfig.searchFields.includes(field.name) ? "default" : "outline"}
onClick={() => toggleSearchField(field.name)}
className="p-1 text-xs" style={{ fontSize: "12px" }}
className="p-1 text-xs"
title={localConfig.searchFields.includes(field.name) ? "검색 필드에서 제거" : "검색 필드로 추가"}
>
<Search className="h-3 w-3" />
@@ -341,7 +372,7 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
size="sm"
variant="destructive"
onClick={() => removeDisplayField(index)}
className="p-1 text-xs" style={{ fontSize: "12px" }}
className="p-1 text-xs"
>
<Trash2 className="h-3 w-3" />
</Button>
@@ -362,9 +393,10 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
<Input
id="placeholder"
value={localConfig.placeholder || ""}
onChange={(e) => updateConfig("placeholder", e.target.value)}
onChange={(e) => updateConfigLocal("placeholder", e.target.value)}
onBlur={handleInputBlur}
placeholder="엔티티를 선택하세요"
className="text-xs" style={{ fontSize: "12px" }}
className="text-xs"
/>
</div>
@@ -375,9 +407,10 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
<Input
id="emptyMessage"
value={localConfig.emptyMessage || ""}
onChange={(e) => updateConfig("emptyMessage", e.target.value)}
onChange={(e) => updateConfigLocal("emptyMessage", e.target.value)}
onBlur={handleInputBlur}
placeholder="검색 결과가 없습니다"
className="text-xs" style={{ fontSize: "12px" }}
className="text-xs"
/>
</div>
@@ -393,7 +426,7 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
onChange={(e) => updateConfig("minSearchLength", parseInt(e.target.value))}
min={0}
max={10}
className="text-xs" style={{ fontSize: "12px" }}
className="text-xs"
/>
</div>
@@ -408,7 +441,7 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
onChange={(e) => updateConfig("pageSize", parseInt(e.target.value))}
min={5}
max={100}
className="text-xs" style={{ fontSize: "12px" }}
className="text-xs"
/>
</div>
</div>
@@ -462,7 +495,7 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
}
}}
placeholder='{"status": "active", "department": "IT"}'
className="font-mono text-xs" style={{ fontSize: "12px" }}
className="font-mono text-xs"
rows={3}
/>
<p className="text-muted-foreground text-xs">API JSON .</p>

View File

@@ -113,7 +113,7 @@ export const FileConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-xs" style={{ fontSize: "12px" }}>
<CardTitle className="flex items-center gap-2 text-xs">
<Upload className="h-4 w-4" />
</CardTitle>
@@ -133,7 +133,7 @@ export const FileConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
value={localConfig.uploadText || ""}
onChange={(e) => updateConfig("uploadText", e.target.value)}
placeholder="파일을 선택하거나 여기에 드래그하세요"
className="text-xs" style={{ fontSize: "12px" }}
className="text-xs"
/>
</div>
@@ -146,7 +146,7 @@ export const FileConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
value={localConfig.browseText || ""}
onChange={(e) => updateConfig("browseText", e.target.value)}
placeholder="파일 선택"
className="text-xs" style={{ fontSize: "12px" }}
className="text-xs"
/>
</div>
@@ -196,7 +196,7 @@ export const FileConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
min={0.1}
max={1024}
step={0.1}
className="flex-1 text-xs" style={{ fontSize: "12px" }}
className="flex-1 text-xs"
/>
<span className="text-muted-foreground text-xs">MB</span>
</div>
@@ -214,7 +214,7 @@ export const FileConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
onChange={(e) => updateConfig("maxFiles", parseInt(e.target.value))}
min={1}
max={100}
className="text-xs" style={{ fontSize: "12px" }}
className="text-xs"
/>
</div>
)}
@@ -257,7 +257,7 @@ export const FileConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
value={newFileType}
onChange={(e) => setNewFileType(e.target.value)}
placeholder=".pdf 또는 pdf"
className="flex-1 text-xs" style={{ fontSize: "12px" }}
className="flex-1 text-xs"
/>
<Button size="sm" onClick={addFileType} disabled={!newFileType.trim()} className="text-xs">

View File

@@ -236,11 +236,11 @@ export const FlowVisibilityConfigPanel: React.FC<FlowVisibilityConfigPanelProps>
return (
<div className="space-y-4">
<div className="space-y-1">
<h4 className="flex items-center gap-2 text-xs font-medium" style={{ fontSize: "12px" }}>
<h4 className="flex items-center gap-2 text-xs font-medium">
<Workflow className="h-4 w-4" />
</h4>
<p className="text-muted-foreground text-xs" style={{ fontSize: "12px" }}>
<p className="text-muted-foreground text-xs">
</p>
</div>
@@ -256,7 +256,7 @@ export const FlowVisibilityConfigPanel: React.FC<FlowVisibilityConfigPanelProps>
setTimeout(() => applyConfig(), 0);
}}
/>
<Label htmlFor="flow-control-enabled" className="text-xs font-medium" style={{ fontSize: "12px" }}>
<Label htmlFor="flow-control-enabled" className="text-xs font-medium">
</Label>
</div>
@@ -265,7 +265,7 @@ export const FlowVisibilityConfigPanel: React.FC<FlowVisibilityConfigPanelProps>
<>
{/* 대상 플로우 선택 */}
<div className="space-y-2">
<Label className="text-xs font-medium" style={{ fontSize: "12px" }}>
<Label className="text-xs font-medium">
</Label>
<Select
@@ -275,7 +275,7 @@ export const FlowVisibilityConfigPanel: React.FC<FlowVisibilityConfigPanelProps>
setTimeout(() => applyConfig(), 0);
}}
>
<SelectTrigger className="h-6 text-xs" style={{ fontSize: "12px" }}>
<SelectTrigger className="h-6 text-xs">
<SelectValue placeholder="플로우 위젯 선택" />
</SelectTrigger>
<SelectContent>
@@ -283,7 +283,7 @@ export const FlowVisibilityConfigPanel: React.FC<FlowVisibilityConfigPanelProps>
const flowConfig = (fw as any).componentConfig || {};
const flowName = flowConfig.flowName || `플로우 ${fw.id}`;
return (
<SelectItem key={fw.id} value={fw.id} style={{ fontSize: "12px" }}>
<SelectItem key={fw.id} value={fw.id} className="text-xs">
{flowName}
</SelectItem>
);
@@ -298,7 +298,7 @@ export const FlowVisibilityConfigPanel: React.FC<FlowVisibilityConfigPanelProps>
{/* 단계 선택 */}
<div className="space-y-3">
<div className="flex items-center justify-between">
<Label className="text-xs font-medium" style={{ fontSize: "12px" }}>
<Label className="text-xs font-medium">
</Label>
<div className="flex gap-1">
@@ -307,7 +307,6 @@ export const FlowVisibilityConfigPanel: React.FC<FlowVisibilityConfigPanelProps>
size="sm"
onClick={selectAll}
className="h-7 px-2 text-xs"
style={{ fontSize: "12px" }}
>
</Button>
@@ -316,7 +315,6 @@ export const FlowVisibilityConfigPanel: React.FC<FlowVisibilityConfigPanelProps>
size="sm"
onClick={selectNone}
className="h-7 px-2 text-xs"
style={{ fontSize: "12px" }}
>
</Button>
@@ -325,7 +323,6 @@ export const FlowVisibilityConfigPanel: React.FC<FlowVisibilityConfigPanelProps>
size="sm"
onClick={invertSelection}
className="h-7 px-2 text-xs"
style={{ fontSize: "12px" }}
>
</Button>
@@ -347,9 +344,8 @@ export const FlowVisibilityConfigPanel: React.FC<FlowVisibilityConfigPanelProps>
<Label
htmlFor={`step-${step.id}`}
className="flex flex-1 items-center gap-2 text-xs"
style={{ fontSize: "12px" }}
>
<Badge variant="outline" className="text-xs" style={{ fontSize: "12px" }}>
<Badge variant="outline" className="text-xs">
Step {step.stepOrder}
</Badge>
<span>{step.stepName}</span>
@@ -363,7 +359,7 @@ export const FlowVisibilityConfigPanel: React.FC<FlowVisibilityConfigPanelProps>
{/* 정렬 방식 */}
<div className="space-y-2">
<Label htmlFor="group-align" className="text-xs font-medium" style={{ fontSize: "12px" }}>
<Label htmlFor="group-align" className="text-xs font-medium">
</Label>
<Select
@@ -373,23 +369,23 @@ export const FlowVisibilityConfigPanel: React.FC<FlowVisibilityConfigPanelProps>
onUpdateProperty("webTypeConfig.flowVisibilityConfig.groupAlign", value);
}}
>
<SelectTrigger id="group-align" className="h-6 text-xs" style={{ fontSize: "12px" }}>
<SelectTrigger id="group-align" className="h-6 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="start" style={{ fontSize: "12px" }}>
<SelectItem value="start" className="text-xs">
</SelectItem>
<SelectItem value="center" style={{ fontSize: "12px" }}>
<SelectItem value="center" className="text-xs">
</SelectItem>
<SelectItem value="end" style={{ fontSize: "12px" }}>
<SelectItem value="end" className="text-xs">
</SelectItem>
<SelectItem value="space-between" style={{ fontSize: "12px" }}>
<SelectItem value="space-between" className="text-xs">
</SelectItem>
<SelectItem value="space-around" style={{ fontSize: "12px" }}>
<SelectItem value="space-around" className="text-xs">
</SelectItem>
</SelectContent>

View File

@@ -55,7 +55,7 @@ export function FlowWidgetConfigPanel({ config = {}, onChange }: FlowWidgetConfi
{loading ? (
<div className="flex items-center gap-2 rounded-md border px-3 py-2">
<Loader2 className="h-4 w-4 animate-spin" />
<span className="text-muted-foreground text-xs" style={{ fontSize: "12px" }}> ...</span>
<span className="text-muted-foreground text-xs"> ...</span>
</div>
) : (
<>

View File

@@ -56,7 +56,7 @@ export const NumberConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-xs" style={{ fontSize: "12px" }}> </CardTitle>
<CardTitle className="flex items-center gap-2 text-xs"> </CardTitle>
<CardDescription className="text-xs"> .</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
@@ -73,7 +73,7 @@ export const NumberConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
value={localConfig.placeholder || ""}
onChange={(e) => updateConfig("placeholder", e.target.value)}
placeholder="숫자를 입력하세요"
className="text-xs" style={{ fontSize: "12px" }}
className="text-xs"
/>
</div>
@@ -88,7 +88,7 @@ export const NumberConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
value={localConfig.min ?? ""}
onChange={(e) => updateConfig("min", e.target.value ? parseFloat(e.target.value) : undefined)}
placeholder="0"
className="text-xs" style={{ fontSize: "12px" }}
className="text-xs"
/>
</div>
<div className="space-y-2">
@@ -101,7 +101,7 @@ export const NumberConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
value={localConfig.max ?? ""}
onChange={(e) => updateConfig("max", e.target.value ? parseFloat(e.target.value) : undefined)}
placeholder="100"
className="text-xs" style={{ fontSize: "12px" }}
className="text-xs"
/>
</div>
</div>
@@ -118,7 +118,7 @@ export const NumberConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
placeholder="1"
min="0"
step="0.01"
className="text-xs" style={{ fontSize: "12px" }}
className="text-xs"
/>
<p className="text-muted-foreground text-xs">/ </p>
</div>
@@ -158,7 +158,7 @@ export const NumberConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
placeholder="2"
min="0"
max="10"
className="text-xs" style={{ fontSize: "12px" }}
className="text-xs"
/>
</div>
)}
@@ -223,7 +223,7 @@ export const NumberConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
min={localConfig.min}
max={localConfig.max}
step={localConfig.step}
className="text-xs" style={{ fontSize: "12px" }}
className="text-xs"
/>
<div className="text-muted-foreground mt-2 text-xs">
{localConfig.format === "currency" && "통화 형식으로 표시됩니다."}

View File

@@ -168,7 +168,7 @@ export const RadioConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-xs" style={{ fontSize: "12px" }}>
<CardTitle className="flex items-center gap-2 text-xs">
<Radio className="h-4 w-4" />
</CardTitle>
@@ -188,7 +188,7 @@ export const RadioConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
value={localConfig.groupLabel || ""}
onChange={(e) => updateConfig("groupLabel", e.target.value)}
placeholder="라디오버튼 그룹 제목"
className="text-xs" style={{ fontSize: "12px" }}
className="text-xs"
/>
</div>
@@ -201,7 +201,7 @@ export const RadioConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
value={localConfig.groupName || ""}
onChange={(e) => updateConfig("groupName", e.target.value)}
placeholder="자동 생성 (필드명 기반)"
className="text-xs" style={{ fontSize: "12px" }}
className="text-xs"
/>
<p className="text-muted-foreground text-xs"> .</p>
</div>
@@ -252,19 +252,19 @@ export const RadioConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
value={newOptionLabel}
onChange={(e) => setNewOptionLabel(e.target.value)}
placeholder="라벨"
className="flex-1 text-xs" style={{ fontSize: "12px" }}
className="flex-1 text-xs"
/>
<Input
value={newOptionValue}
onChange={(e) => setNewOptionValue(e.target.value)}
placeholder="값"
className="flex-1 text-xs" style={{ fontSize: "12px" }}
className="flex-1 text-xs"
/>
<Button
size="sm"
onClick={addOption}
disabled={!newOptionLabel.trim() || !newOptionValue.trim()}
className="text-xs" style={{ fontSize: "12px" }}
className="text-xs"
>
<Plus className="h-3 w-3" />
</Button>
@@ -278,7 +278,7 @@ export const RadioConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
value={bulkOptions}
onChange={(e) => setBulkOptions(e.target.value)}
placeholder="한 줄당 하나씩 입력하세요.&#10;라벨만 입력하면 값과 동일하게 설정됩니다.&#10;라벨|값 형식으로 입력하면 별도 값을 설정할 수 있습니다.&#10;&#10;예시:&#10;서울&#10;부산&#10;대구시|daegu"
className="h-20 text-xs" style={{ fontSize: "12px" }}
className="h-20 text-xs"
/>
<Button size="sm" onClick={addBulkOptions} disabled={!bulkOptions.trim()} className="text-xs">
@@ -295,13 +295,13 @@ export const RadioConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
value={option.label}
onChange={(e) => updateOption(index, "label", e.target.value)}
placeholder="라벨"
className="flex-1 text-xs" style={{ fontSize: "12px" }}
className="flex-1 text-xs"
/>
<Input
value={option.value}
onChange={(e) => updateOption(index, "value", e.target.value)}
placeholder="값"
className="flex-1 text-xs" style={{ fontSize: "12px" }}
className="flex-1 text-xs"
/>
<Switch
checked={!option.disabled}
@@ -328,7 +328,7 @@ export const RadioConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
id="defaultValue"
value={localConfig.defaultValue || ""}
onChange={(e) => updateConfig("defaultValue", e.target.value)}
className="w-full rounded-md border px-3 py-1 text-xs" style={{ fontSize: "12px" }}
className="w-full rounded-md border px-3 py-1 text-xs"
>
<option value=""> </option>
{localConfig.options.map((option, index) => (
@@ -390,7 +390,7 @@ export const RadioConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
disabled={localConfig.readonly || option.disabled}
required={localConfig.required && index === 0} // 첫 번째에만 required 표시
defaultChecked={localConfig.defaultValue === option.value}
className="text-xs" style={{ fontSize: "12px" }}
className="text-xs"
/>
<Label htmlFor={`preview-radio-${index}`} className="text-xs">
{option.label}

View File

@@ -153,7 +153,7 @@ export const SelectConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-xs" style={{ fontSize: "12px" }}>
<CardTitle className="flex items-center gap-2 text-xs">
<List className="h-4 w-4" />
</CardTitle>
@@ -173,7 +173,7 @@ export const SelectConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
value={localConfig.placeholder || ""}
onChange={(e) => updateConfig("placeholder", e.target.value)}
placeholder="선택하세요"
className="text-xs" style={{ fontSize: "12px" }}
className="text-xs"
/>
</div>
@@ -186,7 +186,7 @@ export const SelectConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
value={localConfig.emptyMessage || ""}
onChange={(e) => updateConfig("emptyMessage", e.target.value)}
placeholder="선택 가능한 옵션이 없습니다"
className="text-xs" style={{ fontSize: "12px" }}
className="text-xs"
/>
</div>
@@ -247,19 +247,19 @@ export const SelectConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
value={newOptionLabel}
onChange={(e) => setNewOptionLabel(e.target.value)}
placeholder="라벨"
className="flex-1 text-xs" style={{ fontSize: "12px" }}
className="flex-1 text-xs"
/>
<Input
value={newOptionValue}
onChange={(e) => setNewOptionValue(e.target.value)}
placeholder="값"
className="flex-1 text-xs" style={{ fontSize: "12px" }}
className="flex-1 text-xs"
/>
<Button
size="sm"
onClick={addOption}
disabled={!newOptionLabel.trim() || !newOptionValue.trim()}
className="text-xs" style={{ fontSize: "12px" }}
className="text-xs"
>
<Plus className="h-3 w-3" />
</Button>
@@ -273,7 +273,7 @@ export const SelectConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
value={bulkOptions}
onChange={(e) => setBulkOptions(e.target.value)}
placeholder="한 줄당 하나씩 입력하세요.&#10;라벨만 입력하면 값과 동일하게 설정됩니다.&#10;라벨|값 형식으로 입력하면 별도 값을 설정할 수 있습니다.&#10;&#10;예시:&#10;서울&#10;부산&#10;대구시|daegu"
className="h-20 text-xs" style={{ fontSize: "12px" }}
className="h-20 text-xs"
/>
<Button size="sm" onClick={addBulkOptions} disabled={!bulkOptions.trim()} className="text-xs">
@@ -290,13 +290,13 @@ export const SelectConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
value={option.label}
onChange={(e) => updateOption(index, "label", e.target.value)}
placeholder="라벨"
className="flex-1 text-xs" style={{ fontSize: "12px" }}
className="flex-1 text-xs"
/>
<Input
value={option.value}
onChange={(e) => updateOption(index, "value", e.target.value)}
placeholder="값"
className="flex-1 text-xs" style={{ fontSize: "12px" }}
className="flex-1 text-xs"
/>
<Switch
checked={!option.disabled}
@@ -323,7 +323,7 @@ export const SelectConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
id="defaultValue"
value={localConfig.defaultValue || ""}
onChange={(e) => updateConfig("defaultValue", e.target.value)}
className="w-full rounded-md border px-3 py-1 text-xs" style={{ fontSize: "12px" }}
className="w-full rounded-md border px-3 py-1 text-xs"
>
<option value=""> </option>
{localConfig.options.map((option, index) => (
@@ -376,7 +376,7 @@ export const SelectConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
disabled={localConfig.readonly}
required={localConfig.required}
multiple={localConfig.multiple}
className="w-full rounded-md border px-3 py-1 text-xs" style={{ fontSize: "12px" }}
className="w-full rounded-md border px-3 py-1 text-xs"
defaultValue={localConfig.defaultValue}
>
<option value="" disabled>

View File

@@ -0,0 +1,404 @@
"use client";
import React, { useEffect, useState } from "react";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Switch } from "@/components/ui/switch";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
import { Check, ChevronsUpDown, Plus, X, GripVertical, Loader2 } from "lucide-react";
import { cn } from "@/lib/utils";
import type { TabItem, TabsComponent } from "@/types/screen-management";
interface TabsConfigPanelProps {
config: any;
onChange: (config: any) => void;
}
interface ScreenInfo {
screenId: number;
screenName: string;
screenCode: string;
tableName: string;
}
export function TabsConfigPanel({ config, onChange }: TabsConfigPanelProps) {
const [screens, setScreens] = useState<ScreenInfo[]>([]);
const [loading, setLoading] = useState(false);
const [localTabs, setLocalTabs] = useState<TabItem[]>(config.tabs || []);
// 화면 목록 로드
useEffect(() => {
const loadScreens = async () => {
try {
setLoading(true);
// API 클라이언트 동적 import (named export 사용)
const { apiClient } = await import("@/lib/api/client");
// 전체 화면 목록 조회 (페이징 사이즈 크게)
const response = await apiClient.get("/screen-management/screens", {
params: { size: 1000 }
});
console.log("화면 목록 조회 성공:", response.data);
if (response.data.success && response.data.data) {
setScreens(response.data.data);
}
} catch (error: any) {
console.error("Failed to load screens:", error);
console.error("Error response:", error.response?.data);
} finally {
setLoading(false);
}
};
loadScreens();
}, []);
// 컴포넌트 변경 시 로컬 상태 동기화 (초기화만, 입력 중에는 동기화하지 않음)
const [isUserEditing, setIsUserEditing] = useState(false);
useEffect(() => {
// 사용자가 입력 중이 아닐 때만 동기화
if (!isUserEditing) {
setLocalTabs(config.tabs || []);
}
}, [config.tabs, isUserEditing]);
// 탭 추가
const handleAddTab = () => {
const newTab: TabItem = {
id: `tab-${Date.now()}`,
label: `새 탭 ${localTabs.length + 1}`,
order: localTabs.length,
disabled: false,
};
const updatedTabs = [...localTabs, newTab];
setLocalTabs(updatedTabs);
onChange({ ...config, tabs: updatedTabs });
};
// 탭 제거
const handleRemoveTab = (tabId: string) => {
const updatedTabs = localTabs.filter((tab) => tab.id !== tabId);
setLocalTabs(updatedTabs);
onChange({ ...config, tabs: updatedTabs });
};
// 탭 라벨 변경 (입력 중)
const handleLabelChange = (tabId: string, label: string) => {
setIsUserEditing(true);
const updatedTabs = localTabs.map((tab) => (tab.id === tabId ? { ...tab, label } : tab));
setLocalTabs(updatedTabs);
// onChange는 onBlur에서 호출
};
// 탭 라벨 변경 완료 (포커스 아웃 시)
const handleLabelBlur = () => {
setIsUserEditing(false);
onChange({ ...config, tabs: localTabs });
};
// 탭 화면 선택
const handleScreenSelect = (tabId: string, screenId: number, screenName: string) => {
const updatedTabs = localTabs.map((tab) => (tab.id === tabId ? { ...tab, screenId, screenName } : tab));
setLocalTabs(updatedTabs);
onChange({ ...config, tabs: updatedTabs });
};
// 탭 비활성화 토글
const handleDisabledToggle = (tabId: string, disabled: boolean) => {
const updatedTabs = localTabs.map((tab) => (tab.id === tabId ? { ...tab, disabled } : tab));
setLocalTabs(updatedTabs);
onChange({ ...config, tabs: updatedTabs });
};
// 탭 순서 변경
const handleMoveTab = (tabId: string, direction: "up" | "down") => {
const index = localTabs.findIndex((tab) => tab.id === tabId);
if (
(direction === "up" && index === 0) ||
(direction === "down" && index === localTabs.length - 1)
) {
return;
}
const newTabs = [...localTabs];
const targetIndex = direction === "up" ? index - 1 : index + 1;
[newTabs[index], newTabs[targetIndex]] = [newTabs[targetIndex], newTabs[index]];
// order 값 재조정
const updatedTabs = newTabs.map((tab, idx) => ({ ...tab, order: idx }));
setLocalTabs(updatedTabs);
onChange({ ...config, tabs: updatedTabs });
};
return (
<div className="space-y-6 p-4">
<div>
<h3 className="mb-4 text-sm font-semibold"> </h3>
<div className="space-y-4">
{/* 탭 방향 */}
<div>
<Label className="text-xs sm:text-sm"> </Label>
<Select
value={config.orientation || "horizontal"}
onValueChange={(value: "horizontal" | "vertical") =>
onChange({ ...config, orientation: value })
}
>
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="horizontal"></SelectItem>
<SelectItem value="vertical"></SelectItem>
</SelectContent>
</Select>
</div>
{/* 탭 스타일 */}
<div>
<Label className="text-xs sm:text-sm"> </Label>
<Select
value={config.variant || "default"}
onValueChange={(value: "default" | "pills" | "underline") =>
onChange({ ...config, variant: value })
}
>
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="default"></SelectItem>
<SelectItem value="pills"></SelectItem>
<SelectItem value="underline"></SelectItem>
</SelectContent>
</Select>
</div>
{/* 선택 상태 유지 */}
<div className="flex items-center justify-between">
<div>
<Label className="text-xs sm:text-sm"> </Label>
<p className="text-muted-foreground text-[10px] sm:text-xs">
</p>
</div>
<Switch
checked={config.persistSelection || false}
onCheckedChange={(checked) => onChange({ ...config, persistSelection: checked })}
/>
</div>
{/* 탭 닫기 버튼 */}
<div className="flex items-center justify-between">
<div>
<Label className="text-xs sm:text-sm"> </Label>
<p className="text-muted-foreground text-[10px] sm:text-xs">
</p>
</div>
<Switch
checked={config.allowCloseable || false}
onCheckedChange={(checked) => onChange({ ...config, allowCloseable: checked })}
/>
</div>
</div>
</div>
<div>
<div className="mb-4 flex items-center justify-between">
<h3 className="text-sm font-semibold"> </h3>
<Button
onClick={handleAddTab}
size="sm"
variant="outline"
className="h-8 text-xs sm:h-9 sm:text-sm"
>
<Plus className="mr-1 h-3 w-3 sm:h-4 sm:w-4" />
</Button>
</div>
{localTabs.length === 0 ? (
<div className="rounded-lg border border-dashed p-8 text-center">
<p className="text-muted-foreground text-sm"> </p>
<p className="text-muted-foreground mt-1 text-xs">
</p>
</div>
) : (
<div className="space-y-3">
{localTabs.map((tab, index) => (
<div
key={tab.id}
className="rounded-lg border bg-card p-3 shadow-sm"
>
<div className="mb-3 flex items-center justify-between">
<div className="flex items-center gap-2">
<GripVertical className="h-4 w-4 text-muted-foreground" />
<span className="text-xs font-medium"> {index + 1}</span>
</div>
<div className="flex items-center gap-1">
<Button
onClick={() => handleMoveTab(tab.id, "up")}
disabled={index === 0}
size="sm"
variant="ghost"
className="h-7 w-7 p-0"
>
</Button>
<Button
onClick={() => handleMoveTab(tab.id, "down")}
disabled={index === localTabs.length - 1}
size="sm"
variant="ghost"
className="h-7 w-7 p-0"
>
</Button>
<Button
onClick={() => handleRemoveTab(tab.id)}
size="sm"
variant="ghost"
className="h-7 w-7 p-0 text-destructive hover:text-destructive"
>
<X className="h-3 w-3" />
</Button>
</div>
</div>
<div className="space-y-3">
{/* 탭 라벨 */}
<div>
<Label className="text-xs"> </Label>
<Input
value={tab.label}
onChange={(e) => handleLabelChange(tab.id, e.target.value)}
onBlur={handleLabelBlur}
placeholder="탭 이름"
className="h-8 text-xs sm:h-9 sm:text-sm"
/>
</div>
{/* 화면 선택 */}
<div>
<Label className="text-xs"> </Label>
{loading ? (
<div className="flex items-center gap-2 rounded-md border px-3 py-2">
<Loader2 className="h-3 w-3 animate-spin" />
<span className="text-muted-foreground text-xs"> ...</span>
</div>
) : (
<ScreenSelectCombobox
screens={screens}
selectedScreenId={tab.screenId}
onSelect={(screenId, screenName) =>
handleScreenSelect(tab.id, screenId, screenName)
}
/>
)}
{tab.screenName && (
<p className="text-muted-foreground mt-1 text-xs">
: {tab.screenName}
</p>
)}
</div>
{/* 비활성화 */}
<div className="flex items-center justify-between">
<Label className="text-xs"></Label>
<Switch
checked={tab.disabled || false}
onCheckedChange={(checked) => handleDisabledToggle(tab.id, checked)}
/>
</div>
</div>
</div>
))}
</div>
)}
</div>
</div>
);
}
// 화면 선택 Combobox 컴포넌트
function ScreenSelectCombobox({
screens,
selectedScreenId,
onSelect,
}: {
screens: ScreenInfo[];
selectedScreenId?: number;
onSelect: (screenId: number, screenName: string) => void;
}) {
const [open, setOpen] = useState(false);
const selectedScreen = screens.find((s) => s.screenId === selectedScreenId);
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
className="h-8 w-full justify-between text-xs sm:h-9 sm:text-sm"
>
{selectedScreen ? selectedScreen.screenName : "화면 선택"}
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50 sm:h-4 sm:w-4" />
</Button>
</PopoverTrigger>
<PopoverContent
className="p-0"
style={{ width: "var(--radix-popover-trigger-width)" }}
align="start"
>
<Command>
<CommandInput placeholder="화면 검색..." className="text-xs sm:text-sm" />
<CommandList>
<CommandEmpty className="text-xs sm:text-sm">
.
</CommandEmpty>
<CommandGroup>
{screens.map((screen) => (
<CommandItem
key={screen.screenId}
value={screen.screenName}
onSelect={() => {
onSelect(screen.screenId, screen.screenName);
setOpen(false);
}}
className="text-xs sm:text-sm"
>
<Check
className={cn(
"mr-2 h-3 w-3 sm:h-4 sm:w-4",
selectedScreenId === screen.screenId ? "opacity-100" : "opacity-0"
)}
/>
<div className="flex flex-col">
<span className="font-medium">{screen.screenName}</span>
<span className="text-muted-foreground text-[10px]">
: {screen.screenCode} | : {screen.tableName}
</span>
</div>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
}

View File

@@ -55,7 +55,7 @@ export const TextConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-xs" style={{ fontSize: "12px" }}> </CardTitle>
<CardTitle className="flex items-center gap-2 text-xs"> </CardTitle>
<CardDescription className="text-xs"> .</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
@@ -72,7 +72,7 @@ export const TextConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
value={localConfig.placeholder || ""}
onChange={(e) => updateConfig("placeholder", e.target.value)}
placeholder="입력 안내 텍스트"
className="text-xs" style={{ fontSize: "12px" }}
className="text-xs"
/>
</div>
@@ -88,7 +88,7 @@ export const TextConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
onChange={(e) => updateConfig("minLength", e.target.value ? parseInt(e.target.value) : undefined)}
placeholder="0"
min="0"
className="text-xs" style={{ fontSize: "12px" }}
className="text-xs"
/>
</div>
<div className="space-y-2">
@@ -102,7 +102,7 @@ export const TextConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
onChange={(e) => updateConfig("maxLength", e.target.value ? parseInt(e.target.value) : undefined)}
placeholder="100"
min="1"
className="text-xs" style={{ fontSize: "12px" }}
className="text-xs"
/>
</div>
</div>
@@ -141,7 +141,7 @@ export const TextConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
value={localConfig.pattern || ""}
onChange={(e) => updateConfig("pattern", e.target.value)}
placeholder="예: [A-Za-z0-9]+"
className="font-mono text-xs" style={{ fontSize: "12px" }}
className="font-mono text-xs"
/>
<p className="text-muted-foreground text-xs">JavaScript .</p>
</div>
@@ -219,7 +219,7 @@ export const TextConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
minLength={localConfig.minLength}
pattern={localConfig.pattern}
autoComplete={localConfig.autoComplete}
className="text-xs" style={{ fontSize: "12px" }}
className="text-xs"
/>
</div>
</div>

View File

@@ -68,7 +68,7 @@ export const TextareaConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-xs" style={{ fontSize: "12px" }}>
<CardTitle className="flex items-center gap-2 text-xs">
<AlignLeft className="h-4 w-4" />
</CardTitle>
@@ -88,7 +88,7 @@ export const TextareaConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
value={localConfig.placeholder || ""}
onChange={(e) => updateConfig("placeholder", e.target.value)}
placeholder="내용을 입력하세요"
className="text-xs" style={{ fontSize: "12px" }}
className="text-xs"
/>
</div>
@@ -101,7 +101,7 @@ export const TextareaConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
value={localConfig.defaultValue || ""}
onChange={(e) => updateConfig("defaultValue", e.target.value)}
placeholder="기본 텍스트 내용"
className="text-xs" style={{ fontSize: "12px" }}
className="text-xs"
rows={3}
/>
{localConfig.showCharCount && (
@@ -151,7 +151,7 @@ export const TextareaConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
placeholder="자동 (CSS로 제어)"
min={10}
max={200}
className="text-xs" style={{ fontSize: "12px" }}
className="text-xs"
/>
<p className="text-muted-foreground text-xs"> CSS width로 .</p>
</div>
@@ -203,7 +203,7 @@ export const TextareaConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
}}
placeholder="제한 없음"
min={0}
className="text-xs" style={{ fontSize: "12px" }}
className="text-xs"
/>
</div>
@@ -221,7 +221,7 @@ export const TextareaConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
}}
placeholder="제한 없음"
min={1}
className="text-xs" style={{ fontSize: "12px" }}
className="text-xs"
/>
</div>
@@ -333,7 +333,7 @@ export const TextareaConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
resize: localConfig.resizable ? "both" : "none",
minHeight: localConfig.autoHeight ? "auto" : undefined,
}}
className="text-xs" style={{ fontSize: "12px" }}
className="text-xs"
wrap={localConfig.wrap}
/>
{localConfig.showCharCount && (

View File

@@ -94,7 +94,7 @@ export const FlowButtonGroupDialog: React.FC<FlowButtonGroupDialogProps> = ({
max={100}
value={gap}
onChange={(e) => setGap(Number(e.target.value))}
className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}
className="h-6 w-full px-2 py-0 text-xs"
/>
<Badge variant="outline" className="text-xs">
{gap}px
@@ -109,7 +109,7 @@ export const FlowButtonGroupDialog: React.FC<FlowButtonGroupDialogProps> = ({
</Label>
<Select value={align} onValueChange={(value: any) => setAlign(value)}>
<SelectTrigger id="align" className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}>
<SelectTrigger id="align" className="h-6 w-full px-2 py-0 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>

View File

@@ -181,7 +181,7 @@ export function ComponentsPanel({
onSearchChange(value);
}
}}
className="h-8 pl-8 text-xs" style={{ fontSize: "12px" }}
className="h-8 pl-8 text-xs"
/>
</div>
</div>

View File

@@ -458,7 +458,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
updateSettings({ options: newOptions });
}}
placeholder="옵션명"
className="h-7 text-xs" style={{ fontSize: "12px" }}
className="h-7 text-xs"
/>
<Button
type="button"
@@ -483,7 +483,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
const newOption = { label: "", value: "" };
updateSettings({ options: [...(localSettings.options || []), newOption] });
}}
className="h-7 text-xs" style={{ fontSize: "12px" }}
className="h-7 text-xs"
>
<Plus className="mr-1 h-3 w-3" />
@@ -548,7 +548,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
value={localSettings.min || ""}
onChange={(e) => updateSettings({ min: e.target.value ? Number(e.target.value) : undefined })}
placeholder="최소값"
className="h-7 text-xs" style={{ fontSize: "12px" }}
className="h-7 text-xs"
/>
</div>
<div className="space-y-1">
@@ -558,7 +558,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
value={localSettings.max || ""}
onChange={(e) => updateSettings({ max: e.target.value ? Number(e.target.value) : undefined })}
placeholder="최대값"
className="h-7 text-xs" style={{ fontSize: "12px" }}
className="h-7 text-xs"
/>
</div>
</div>
@@ -571,7 +571,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
value={localSettings.step || "0.01"}
onChange={(e) => updateSettings({ step: e.target.value })}
placeholder="0.01"
className="h-7 text-xs" style={{ fontSize: "12px" }}
className="h-7 text-xs"
/>
</div>
)}
@@ -589,7 +589,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
type="date"
value={localSettings.minDate || ""}
onChange={(e) => updateSettings({ minDate: e.target.value })}
className="h-7 text-xs" style={{ fontSize: "12px" }}
className="h-7 text-xs"
/>
</div>
<div className="space-y-1">
@@ -598,7 +598,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
type="date"
value={localSettings.maxDate || ""}
onChange={(e) => updateSettings({ maxDate: e.target.value })}
className="h-7 text-xs" style={{ fontSize: "12px" }}
className="h-7 text-xs"
/>
</div>
</div>
@@ -626,7 +626,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
value={localSettings.maxLength || ""}
onChange={(e) => updateSettings({ maxLength: e.target.value ? Number(e.target.value) : undefined })}
placeholder="최대 문자 수"
className="h-7 text-xs" style={{ fontSize: "12px" }}
className="h-7 text-xs"
/>
</div>
<div className="space-y-1">
@@ -635,7 +635,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
value={localSettings.placeholder || ""}
onChange={(e) => updateSettings({ placeholder: e.target.value })}
placeholder="입력 안내 텍스트"
className="h-7 text-xs" style={{ fontSize: "12px" }}
className="h-7 text-xs"
/>
</div>
</div>
@@ -652,7 +652,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
value={localSettings.rows || "3"}
onChange={(e) => updateSettings({ rows: Number(e.target.value) })}
placeholder="3"
className="h-7 text-xs" style={{ fontSize: "12px" }}
className="h-7 text-xs"
/>
</div>
<div className="space-y-1">
@@ -662,7 +662,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
value={localSettings.maxLength || ""}
onChange={(e) => updateSettings({ maxLength: e.target.value ? Number(e.target.value) : undefined })}
placeholder="최대 문자 수"
className="h-7 text-xs" style={{ fontSize: "12px" }}
className="h-7 text-xs"
/>
</div>
</div>
@@ -678,7 +678,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
value={localSettings.accept || ""}
onChange={(e) => updateSettings({ accept: e.target.value })}
placeholder=".jpg,.png,.pdf"
className="h-7 text-xs" style={{ fontSize: "12px" }}
className="h-7 text-xs"
/>
</div>
<div className="space-y-1">
@@ -688,7 +688,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
value={localSettings.maxSize ? localSettings.maxSize / 1024 / 1024 : "10"}
onChange={(e) => updateSettings({ maxSize: Number(e.target.value) * 1024 * 1024 })}
placeholder="10"
className="h-7 text-xs" style={{ fontSize: "12px" }}
className="h-7 text-xs"
/>
</div>
<div className="flex items-center space-x-2">
@@ -1132,7 +1132,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
{/* 기본 설정 */}
<Card>
<CardHeader>
<CardTitle className="flex items-center space-x-2 text-xs" style={{ fontSize: "12px" }}>
<CardTitle className="flex items-center space-x-2 text-xs">
<Settings className="h-4 w-4" />
<span> </span>
</CardTitle>
@@ -1184,7 +1184,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
onUpdateComponent({ enableAdd: checked as boolean });
}}
/>
<Label htmlFor="enable-add" className="text-xs" style={{ fontSize: "12px" }}>
<Label htmlFor="enable-add" className="text-xs">
</Label>
</div>
@@ -1198,7 +1198,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
onUpdateComponent({ enableEdit: checked as boolean });
}}
/>
<Label htmlFor="enable-edit" className="text-xs" style={{ fontSize: "12px" }}>
<Label htmlFor="enable-edit" className="text-xs">
</Label>
</div>
@@ -1212,7 +1212,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
onUpdateComponent({ enableDelete: checked as boolean });
}}
/>
<Label htmlFor="enable-delete" className="text-xs" style={{ fontSize: "12px" }}>
<Label htmlFor="enable-delete" className="text-xs">
</Label>
</div>
@@ -1220,7 +1220,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
<div className="grid grid-cols-3 gap-4">
<div className="space-y-2">
<Label htmlFor="add-button-text" className="text-xs" style={{ fontSize: "12px" }}>
<Label htmlFor="add-button-text" className="text-xs">
</Label>
<Input
@@ -1233,12 +1233,12 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
}}
placeholder="추가"
disabled={!localValues.enableAdd}
className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}
className="h-6 w-full px-2 py-0 text-xs"
/>
</div>
<div className="space-y-2">
<Label htmlFor="edit-button-text" className="text-xs" style={{ fontSize: "12px" }}>
<Label htmlFor="edit-button-text" className="text-xs">
</Label>
<Input
@@ -1251,12 +1251,12 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
}}
placeholder="수정"
disabled={!localValues.enableEdit}
className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}
className="h-6 w-full px-2 py-0 text-xs"
/>
</div>
<div className="space-y-2">
<Label htmlFor="delete-button-text" className="text-xs" style={{ fontSize: "12px" }}>
<Label htmlFor="delete-button-text" className="text-xs">
</Label>
<Input
@@ -1269,7 +1269,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
}}
placeholder="삭제"
disabled={!localValues.enableDelete}
className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}
className="h-6 w-full px-2 py-0 text-xs"
/>
</div>
</div>
@@ -1284,7 +1284,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="modal-title" className="text-xs" style={{ fontSize: "12px" }}>
<Label htmlFor="modal-title" className="text-xs">
</Label>
<Input
@@ -1298,12 +1298,12 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
});
}}
placeholder="새 데이터 추가"
className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}
className="h-6 w-full px-2 py-0 text-xs"
/>
</div>
<div className="space-y-2">
<Label htmlFor="modal-width" className="text-xs" style={{ fontSize: "12px" }}>
<Label htmlFor="modal-width" className="text-xs">
</Label>
<select
@@ -1328,7 +1328,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
</div>
<div className="space-y-2">
<Label htmlFor="modal-description" className="text-xs" style={{ fontSize: "12px" }}>
<Label htmlFor="modal-description" className="text-xs">
</Label>
<Input
@@ -1342,13 +1342,13 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
});
}}
placeholder="모달에 표시될 설명을 입력하세요"
className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}
className="h-6 w-full px-2 py-0 text-xs"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="modal-layout" className="text-xs" style={{ fontSize: "12px" }}>
<Label htmlFor="modal-layout" className="text-xs">
</Label>
<select
@@ -1370,7 +1370,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
{localValues.modalLayout === "grid" && (
<div className="space-y-2">
<Label htmlFor="modal-grid-columns" className="text-xs" style={{ fontSize: "12px" }}>
<Label htmlFor="modal-grid-columns" className="text-xs">
</Label>
<select
@@ -1394,7 +1394,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="modal-submit-text" className="text-xs" style={{ fontSize: "12px" }}>
<Label htmlFor="modal-submit-text" className="text-xs">
</Label>
<Input
@@ -1408,12 +1408,12 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
});
}}
placeholder="추가"
className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}
className="h-6 w-full px-2 py-0 text-xs"
/>
</div>
<div className="space-y-2">
<Label htmlFor="modal-cancel-text" className="text-xs" style={{ fontSize: "12px" }}>
<Label htmlFor="modal-cancel-text" className="text-xs">
</Label>
<Input
@@ -1427,7 +1427,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
});
}}
placeholder="취소"
className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}
className="h-6 w-full px-2 py-0 text-xs"
/>
</div>
</div>
@@ -1441,7 +1441,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
<div className="space-y-3">
<div className="space-y-2">
<Label htmlFor="edit-modal-title" className="text-xs" style={{ fontSize: "12px" }}>
<Label htmlFor="edit-modal-title" className="text-xs">
</Label>
<Input
@@ -1455,13 +1455,13 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
});
}}
placeholder="데이터 수정"
className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}
className="h-6 w-full px-2 py-0 text-xs"
/>
<p className="text-xs text-gray-500"> </p>
</div>
<div className="space-y-2">
<Label htmlFor="edit-modal-description" className="text-xs" style={{ fontSize: "12px" }}>
<Label htmlFor="edit-modal-description" className="text-xs">
</Label>
<Input
@@ -1475,7 +1475,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
});
}}
placeholder="선택한 데이터를 수정합니다"
className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}
className="h-6 w-full px-2 py-0 text-xs"
/>
<p className="text-xs text-gray-500"> </p>
</div>
@@ -1494,7 +1494,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
onUpdateComponent({ showSearchButton: checked as boolean });
}}
/>
<Label htmlFor="show-search-button" className="text-xs" style={{ fontSize: "12px" }}>
<Label htmlFor="show-search-button" className="text-xs">
</Label>
</div>
@@ -1509,7 +1509,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
onUpdateComponent({ enableExport: checked as boolean });
}}
/>
<Label htmlFor="enable-export" className="text-xs" style={{ fontSize: "12px" }}>
<Label htmlFor="enable-export" className="text-xs">
</Label>
</div>
@@ -1521,7 +1521,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
<TabsContent value="columns" className="mt-4 max-h-[70vh] overflow-y-auto">
<Card>
<CardHeader>
<CardTitle className="flex items-center space-x-2 text-xs" style={{ fontSize: "12px" }}>
<CardTitle className="flex items-center space-x-2 text-xs">
<Columns className="h-4 w-4" />
<span> </span>
</CardTitle>
@@ -1654,7 +1654,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
}
}}
placeholder="표시명을 입력하세요"
className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}
className="h-6 w-full px-2 py-0 text-xs"
/>
</div>
@@ -1947,7 +1947,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
});
}}
placeholder="고정값 입력..."
className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}
className="h-6 w-full px-2 py-0 text-xs"
/>
</div>
)}
@@ -1967,7 +1967,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
<TabsContent value="filters" className="mt-4 max-h-[70vh] overflow-y-auto">
<Card>
<CardHeader>
<CardTitle className="flex items-center space-x-2 text-xs" style={{ fontSize: "12px" }}>
<CardTitle className="flex items-center space-x-2 text-xs">
<Filter className="h-4 w-4" />
<span> </span>
</CardTitle>
@@ -1995,7 +1995,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
{component.filters.length === 0 ? (
<div className="text-muted-foreground py-8 text-center">
<Filter className="mx-auto mb-2 h-8 w-8 opacity-50" />
<p className="text-xs" style={{ fontSize: "12px" }}> </p>
<p className="text-xs"> </p>
<p className="text-xs"> </p>
</div>
) : (
@@ -2073,7 +2073,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
updateFilter(index, { label: newValue });
}}
placeholder="필터 이름 입력..."
className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}
className="h-6 w-full px-2 py-0 text-xs"
/>
</div>
<p className="text-muted-foreground mt-1 text-xs">
@@ -2192,7 +2192,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
<TabsContent value="modal" className="mt-4 max-h-[70vh] overflow-y-auto">
<Card>
<CardHeader>
<CardTitle className="flex items-center space-x-2 text-xs" style={{ fontSize: "12px" }}>
<CardTitle className="flex items-center space-x-2 text-xs">
<Settings className="h-4 w-4" />
<span> </span>
</CardTitle>
@@ -2342,7 +2342,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
});
}}
/>
<Label htmlFor="show-page-size-selector" className="text-xs" style={{ fontSize: "12px" }}>
<Label htmlFor="show-page-size-selector" className="text-xs">
</Label>
</div>
@@ -2362,7 +2362,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
});
}}
/>
<Label htmlFor="show-page-info" className="text-xs" style={{ fontSize: "12px" }}>
<Label htmlFor="show-page-info" className="text-xs">
</Label>
</div>
@@ -2382,7 +2382,7 @@ const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
});
}}
/>
<Label htmlFor="show-first-last" className="text-xs" style={{ fontSize: "12px" }}>
<Label htmlFor="show-first-last" className="text-xs">
/
</Label>
</div>

View File

@@ -158,7 +158,6 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
}
}}
className="w-full rounded border border-gray-300 px-2 py-1 text-xs"
style={{ fontSize: "12px" }}
/>
</div>
<div>
@@ -196,7 +195,6 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
}
}}
className="w-full rounded border border-gray-300 px-2 py-1 text-xs"
style={{ fontSize: "12px" }}
/>
</div>
</div>
@@ -211,7 +209,6 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
onUpdateProperty(layoutComponent.id, "layoutConfig.grid.gap", parseInt(e.target.value))
}
className="w-full rounded border border-gray-300 px-2 py-1 text-xs"
style={{ fontSize: "12px" }}
/>
</div>
</div>
@@ -256,7 +253,6 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
}
}}
className="w-full rounded border border-gray-300 px-2 py-1 text-xs"
style={{ fontSize: "12px" }}
>
<option value="row"> (row)</option>
<option value="column"> (column)</option>
@@ -316,7 +312,6 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
}
}}
className="w-20 rounded border border-gray-300 px-2 py-1 text-xs"
style={{ fontSize: "12px" }}
/>
<span className="text-xs text-gray-500"></span>
</div>
@@ -332,7 +327,6 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
onUpdateProperty(layoutComponent.id, "layoutConfig.flexbox.gap", parseInt(e.target.value))
}
className="w-full rounded border border-gray-300 px-2 py-1 text-xs"
style={{ fontSize: "12px" }}
/>
</div>
</div>
@@ -348,7 +342,6 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
value={layoutComponent.layoutConfig?.split?.direction || "horizontal"}
onChange={(e) => onUpdateProperty(layoutComponent.id, "layoutConfig.split.direction", e.target.value)}
className="w-full rounded border border-gray-300 px-2 py-1 text-xs"
style={{ fontSize: "12px" }}
>
<option value="horizontal"> </option>
<option value="vertical"> </option>
@@ -398,7 +391,6 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
)
}
className="w-full rounded border border-gray-300 px-2 py-1 text-xs"
style={{ fontSize: "12px" }}
>
<option value=""> </option>
{currentTable.columns?.map((column) => (
@@ -421,7 +413,6 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
)
}
className="w-full rounded border border-gray-300 px-2 py-1 text-xs"
style={{ fontSize: "12px" }}
>
<option value=""> </option>
{currentTable.columns?.map((column) => (
@@ -444,7 +435,6 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
)
}
className="w-full rounded border border-gray-300 px-2 py-1 text-xs"
style={{ fontSize: "12px" }}
>
<option value=""> </option>
{currentTable.columns?.map((column) => (
@@ -467,7 +457,6 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
)
}
className="w-full rounded border border-gray-300 px-2 py-1 text-xs"
style={{ fontSize: "12px" }}
>
<option value=""> </option>
{currentTable.columns?.map((column) => (
@@ -495,7 +484,6 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
);
}}
className="bg-primary text-primary-foreground hover:bg-primary/90 rounded px-2 py-1 text-xs"
style={{ fontSize: "12px" }}
>
+
</button>
@@ -519,7 +507,6 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
);
}}
className="flex-1 rounded border border-gray-300 px-2 py-1 text-xs"
style={{ fontSize: "12px" }}
>
<option value=""> </option>
{currentTable.columns?.map((col) => (
@@ -542,7 +529,6 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
);
}}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90 rounded px-2 py-1 text-xs"
style={{ fontSize: "12px" }}
>
</button>
@@ -578,7 +564,6 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
onUpdateProperty(layoutComponent.id, "layoutConfig.card.cardsPerRow", parseInt(e.target.value))
}
className="w-full rounded border border-gray-300 px-2 py-1 text-xs"
style={{ fontSize: "12px" }}
/>
</div>
@@ -593,7 +578,6 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
onUpdateProperty(layoutComponent.id, "layoutConfig.card.cardSpacing", parseInt(e.target.value))
}
className="w-full rounded border border-gray-300 px-2 py-1 text-xs"
style={{ fontSize: "12px" }}
/>
</div>
</div>
@@ -683,7 +667,6 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
)
}
className="w-full rounded border border-gray-300 px-2 py-1 text-xs"
style={{ fontSize: "12px" }}
/>
</div>
</div>
@@ -711,7 +694,6 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
onUpdateProperty(layoutComponent.id, `zones.${index}.size.width`, e.target.value)
}
className="w-full rounded border border-gray-300 px-2 py-1 text-xs"
style={{ fontSize: "12px" }}
placeholder="100%"
/>
</div>
@@ -724,7 +706,6 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
onUpdateProperty(layoutComponent.id, `zones.${index}.size.height`, e.target.value)
}
className="w-full rounded border border-gray-300 px-2 py-1 text-xs"
style={{ fontSize: "12px" }}
placeholder="auto"
/>
</div>
@@ -1007,7 +988,7 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
<h3 className="font-medium text-gray-900"> </h3>
</div>
<div className="mt-2 flex items-center space-x-2">
<span className="text-muted-foreground text-xs" style={{ fontSize: "12px" }}>
<span className="text-muted-foreground text-xs">
:
</span>
<span className="rounded bg-green-100 px-2 py-1 text-xs font-medium text-green-800">{componentType}</span>
@@ -1057,7 +1038,7 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
<h3 className="font-medium text-gray-900"> </h3>
</div>
<div className="mt-2 flex items-center space-x-2">
<span className="text-muted-foreground text-xs" style={{ fontSize: "12px" }}>
<span className="text-muted-foreground text-xs">
:
</span>
<span className="rounded bg-purple-100 px-2 py-1 text-xs font-medium text-purple-800"> </span>
@@ -1146,14 +1127,14 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
{/* 컴포넌트 정보 */}
<div className="mb-4 space-y-2">
<div className="flex items-center space-x-2">
<span className="text-muted-foreground text-xs" style={{ fontSize: "12px" }}>
<span className="text-muted-foreground text-xs">
:
</span>
<span className="rounded bg-green-100 px-2 py-1 text-xs font-medium text-green-800">{componentId}</span>
</div>
{webType && currentBaseInputType && (
<div className="flex items-center space-x-2">
<span className="text-muted-foreground text-xs" style={{ fontSize: "12px" }}>
<span className="text-muted-foreground text-xs">
:
</span>
<span className="rounded bg-blue-100 px-2 py-1 text-xs font-medium text-blue-800">
@@ -1163,7 +1144,7 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
)}
{selectedComponent.columnName && (
<div className="flex items-center space-x-2">
<span className="text-muted-foreground text-xs" style={{ fontSize: "12px" }}>
<span className="text-muted-foreground text-xs">
:
</span>
<span className="text-xs text-gray-700">{selectedComponent.columnName}</span>
@@ -1375,7 +1356,7 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
<h3 className="font-medium text-gray-900"> </h3>
</div>
<div className="mt-2 flex items-center space-x-2">
<span className="text-muted-foreground text-xs" style={{ fontSize: "12px" }}>
<span className="text-muted-foreground text-xs">
:
</span>
<span className="rounded bg-blue-100 px-2 py-1 text-xs font-medium text-blue-800">
@@ -1390,7 +1371,7 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
<div className="space-y-2">
<label className="block text-sm font-medium text-gray-700"> </label>
<Select value={localDetailType} onValueChange={handleDetailTypeChange}>
<SelectTrigger className="h-6 w-full px-2 py-0 bg-white text-xs" style={{ fontSize: "12px" }}>
<SelectTrigger className="h-6 w-full px-2 py-0 bg-white text-xs">
<SelectValue placeholder="세부 타입을 선택하세요" />
</SelectTrigger>
<SelectContent>

View File

@@ -98,7 +98,7 @@ export const FlowButtonGroupPanel: React.FC<FlowButtonGroupPanelProps> = ({
size="sm"
variant="ghost"
onClick={() => onSelectGroup(groupInfo.buttons.map((b) => b.id))}
className="h-7 px-2 text-xs" style={{ fontSize: "12px" }}
className="h-7 px-2 text-xs"
>
</Button>
@@ -152,7 +152,7 @@ export const FlowButtonGroupPanel: React.FC<FlowButtonGroupPanelProps> = ({
{groupInfo.buttons.map((button) => (
<div
key={button.id}
className="flex items-center gap-2 rounded bg-white px-2 py-1.5 text-xs" style={{ fontSize: "12px" }}
className="flex items-center gap-2 rounded bg-white px-2 py-1.5 text-xs"
>
<div className="h-2 w-2 rounded-full bg-blue-500" />
<span className="flex-1 truncate font-medium">

View File

@@ -214,7 +214,7 @@ export default function LayoutsPanel({
</Badge>
</div>
</div>
<CardTitle className="text-xs" style={{ fontSize: "12px" }}>{layout.name}</CardTitle>
<CardTitle className="text-xs">{layout.name}</CardTitle>
</CardHeader>
<CardContent className="pt-0">
{layout.description && (

View File

@@ -652,7 +652,7 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
}}
className="border-input bg-background text-primary focus:ring-ring h-4 w-4 rounded border focus:ring-2 focus:ring-offset-2"
/>
<Label htmlFor="required" className="text-xs" style={{ fontSize: "12px" }}>
<Label htmlFor="required" className="text-xs">
</Label>
</div>
@@ -668,7 +668,7 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
}}
className="border-input bg-background text-primary focus:ring-ring h-4 w-4 rounded border focus:ring-2 focus:ring-offset-2"
/>
<Label htmlFor="readonly" className="text-xs" style={{ fontSize: "12px" }}>
<Label htmlFor="readonly" className="text-xs">
</Label>
</div>
@@ -990,7 +990,7 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
</>
) : (
<div className="bg-accent col-span-2 rounded-lg p-3 text-center">
<p className="text-primary text-xs" style={{ fontSize: "12px" }}> </p>
<p className="text-primary text-xs"> </p>
<p className="mt-1 text-xs text-blue-500"> </p>
</div>
)}

View File

@@ -84,7 +84,7 @@ const ResolutionPanel: React.FC<ResolutionPanelProps> = ({ currentResolution, on
<div className="space-y-2">
<Label className="text-xs font-medium"> </Label>
<Select value={selectedPreset} onValueChange={handlePresetChange}>
<SelectTrigger className="h-6 w-full px-2 py-0" style={{ fontSize: "12px" }}>
<SelectTrigger className=" text-xsh-6 w-full px-2 py-0">
<SelectValue placeholder="해상도를 선택하세요" />
</SelectTrigger>
<SelectContent>
@@ -147,8 +147,7 @@ const ResolutionPanel: React.FC<ResolutionPanelProps> = ({ currentResolution, on
placeholder="1920"
min="1"
step="1"
className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}
style={{ fontSize: "12px" }}
className="h-6 w-full px-2 py-0 text-xs"
/>
</div>
<div className="space-y-1">
@@ -160,16 +159,14 @@ const ResolutionPanel: React.FC<ResolutionPanelProps> = ({ currentResolution, on
placeholder="1080"
min="1"
step="1"
className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}
style={{ fontSize: "12px" }}
className="h-6 w-full px-2 py-0 text-xs"
/>
</div>
</div>
<Button
onClick={handleCustomResolution}
size="sm"
className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}
style={{ fontSize: "12px" }}
className="h-6 w-full px-2 py-0 text-xs"
>
</Button>

View File

@@ -109,7 +109,7 @@ export const RowSettingsPanel: React.FC<RowSettingsPanelProps> = ({ row, onUpdat
variant={row.gap === preset ? "default" : "outline"}
size="sm"
onClick={() => onUpdateRow({ gap: preset })}
className="text-xs" style={{ fontSize: "12px" }}
className="text-xs"
>
{GAP_PRESETS[preset].label}
</Button>
@@ -130,7 +130,7 @@ export const RowSettingsPanel: React.FC<RowSettingsPanelProps> = ({ row, onUpdat
variant={row.padding === preset ? "default" : "outline"}
size="sm"
onClick={() => onUpdateRow({ padding: preset })}
className="text-xs" style={{ fontSize: "12px" }}
className="text-xs"
>
{GAP_PRESETS[preset].label}
</Button>

View File

@@ -528,7 +528,7 @@ export const TemplatesPanel: React.FC<TemplatesPanelProps> = ({ onDragStart }) =
<div className="flex items-center justify-between rounded-xl bg-amber-50/80 border border-amber-200/60 p-3 text-amber-800 mb-4">
<div className="flex items-center space-x-2">
<Info className="h-4 w-4" />
<span className="text-xs" style={{ fontSize: "12px" }}>릿 , 릿 </span>
<span className="text-xs">릿 , 릿 </span>
</div>
<Button size="sm" variant="outline" onClick={() => refetch()} className="border-amber-300 text-amber-700 hover:bg-amber-100">
<RefreshCw className="h-4 w-4" />

View File

@@ -1,6 +1,6 @@
"use client";
import React, { useState, useEffect } from "react";
import React, { useState, useEffect, Suspense } from "react";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
@@ -341,14 +341,20 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
<Settings className="h-4 w-4 text-primary" />
<h3 className="text-sm font-semibold">{definition.name} </h3>
</div>
<ConfigPanelComponent
config={config}
onChange={handleConfigChange}
tables={tables} // 테이블 정보 전달
allTables={allTables} // 🆕 전체 테이블 목록 전달 (selected-items-detail-input 등에서 사용)
screenTableName={selectedComponent.tableName || currentTable?.tableName || currentTableName} // 🔧 화면 테이블명 전달
tableColumns={currentTable?.columns || []} // 🔧 테이블 컬럼 정보 전달
/>
<Suspense fallback={
<div className="flex items-center justify-center py-8">
<div className="text-sm text-muted-foreground"> ...</div>
</div>
}>
<ConfigPanelComponent
config={config}
onChange={handleConfigChange}
tables={tables} // 테이블 정보 전달
allTables={allTables} // 🆕 전체 테이블 목록 전달 (selected-items-detail-input 등에서 사용)
screenTableName={selectedComponent.tableName || currentTable?.tableName || currentTableName} // 🔧 화면 테이블명 전달
tableColumns={currentTable?.columns || []} // 🔧 테이블 컬럼 정보 전달
/>
</Suspense>
</div>
);
};
@@ -712,8 +718,6 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
onChange={(e) => handleUpdate("label", e.target.value)}
placeholder="라벨"
className="h-6 w-full px-2 py-0 text-xs"
style={{ fontSize: "12px" }}
style={{ fontSize: "12px" }}
/>
</div>
<div className="space-y-1">
@@ -749,7 +753,6 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
step={1}
placeholder="10"
className="h-6 w-full px-2 py-0 text-xs"
style={{ fontSize: "12px" }}
/>
</div>
</div>
@@ -763,8 +766,6 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
onChange={(e) => handleUpdate("placeholder", e.target.value)}
placeholder="입력 안내 텍스트"
className="h-6 w-full px-2 py-0 text-xs"
style={{ fontSize: "12px" }}
style={{ fontSize: "12px" }}
/>
</div>
)}
@@ -778,8 +779,6 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
onChange={(e) => handleUpdate("title", e.target.value)}
placeholder="제목"
className="h-6 w-full px-2 py-0 text-xs"
style={{ fontSize: "12px" }}
style={{ fontSize: "12px" }}
/>
</div>
)}
@@ -793,8 +792,6 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
onChange={(e) => handleUpdate("description", e.target.value)}
placeholder="설명"
className="h-6 w-full px-2 py-0 text-xs"
style={{ fontSize: "12px" }}
style={{ fontSize: "12px" }}
/>
</div>
)}
@@ -836,7 +833,6 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
}
}}
className="h-6 w-full px-2 py-0 text-xs"
style={{ fontSize: "12px" }}
/>
</div>
</div>
@@ -848,8 +844,7 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
value={currentPosition.z || 1}
onChange={(e) => handleUpdate("position.z", parseInt(e.target.value) || 1)}
className="h-6 w-full px-2 py-0 text-xs"
style={{ fontSize: "12px" }}
style={{ fontSize: "12px" }}
className="text-xs"
/>
</div>
</div>
@@ -867,8 +862,7 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
value={selectedComponent.style?.labelText || selectedComponent.label || ""}
onChange={(e) => handleUpdate("style.labelText", e.target.value)}
className="h-6 w-full px-2 py-0 text-xs"
style={{ fontSize: "12px" }}
style={{ fontSize: "12px" }}
className="text-xs"
/>
</div>
<div className="grid grid-cols-2 gap-2">
@@ -878,8 +872,7 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
value={selectedComponent.style?.labelFontSize || "12px"}
onChange={(e) => handleUpdate("style.labelFontSize", e.target.value)}
className="h-6 w-full px-2 py-0 text-xs"
style={{ fontSize: "12px" }}
style={{ fontSize: "12px" }}
className="text-xs"
/>
</div>
<div className="space-y-1">
@@ -889,8 +882,7 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
value={selectedComponent.style?.labelColor || "#212121"}
onChange={(e) => handleUpdate("style.labelColor", e.target.value)}
className="h-6 w-full px-2 py-0 text-xs"
style={{ fontSize: "12px" }}
style={{ fontSize: "12px" }}
className="text-xs"
/>
</div>
</div>
@@ -901,8 +893,7 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
value={selectedComponent.style?.labelMarginBottom || "4px"}
onChange={(e) => handleUpdate("style.labelMarginBottom", e.target.value)}
className="h-6 w-full px-2 py-0 text-xs"
style={{ fontSize: "12px" }}
style={{ fontSize: "12px" }}
className="text-xs"
/>
</div>
<div className="flex items-center space-x-2 pt-5">
@@ -1053,7 +1044,7 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
<div>
<Label> </Label>
<Select value={localComponentDetailType || webType} onValueChange={handleDetailTypeChange}>
<SelectTrigger className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}>
<SelectTrigger className="h-6 w-full px-2 py-0 text-xs">
<SelectValue placeholder="세부 타입 선택" />
</SelectTrigger>
<SelectContent>
@@ -1260,7 +1251,7 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
<div>
<Label> </Label>
<Select value={widget.webType} onValueChange={(value) => handleUpdate("webType", value)}>
<SelectTrigger className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}>
<SelectTrigger className="h-6 w-full px-2 py-0 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>

View File

@@ -109,7 +109,7 @@ export const WebTypeConfigPanel: React.FC<WebTypeConfigPanelProps> = ({ webType,
<>
<Separator />
<div className="flex items-center justify-between">
<Label htmlFor="multiple" className="text-xs" style={{ fontSize: "12px" }}>
<Label htmlFor="multiple" className="text-xs">
</Label>
<Checkbox
@@ -121,7 +121,7 @@ export const WebTypeConfigPanel: React.FC<WebTypeConfigPanelProps> = ({ webType,
/>
</div>
<div className="flex items-center justify-between">
<Label htmlFor="searchable" className="text-xs" style={{ fontSize: "12px" }}>
<Label htmlFor="searchable" className="text-xs">
</Label>
<Checkbox
@@ -259,7 +259,7 @@ export const WebTypeConfigPanel: React.FC<WebTypeConfigPanelProps> = ({ webType,
{baseType === "date" && (
<div className="flex items-center justify-between">
<Label htmlFor="showTime" className="text-xs" style={{ fontSize: "12px" }}>
<Label htmlFor="showTime" className="text-xs">
</Label>
<Checkbox
@@ -395,7 +395,7 @@ export const WebTypeConfigPanel: React.FC<WebTypeConfigPanelProps> = ({ webType,
</div>
<div className="flex items-center justify-between">
<Label htmlFor="fileMultiple" className="text-xs" style={{ fontSize: "12px" }}>
<Label htmlFor="fileMultiple" className="text-xs">
</Label>
<Checkbox

View File

@@ -122,7 +122,7 @@ export const CheckboxTypeConfigPanel: React.FC<CheckboxTypeConfigPanelProps> = (
</Label>
<Select value={localValues.labelPosition} onValueChange={(value) => updateConfig("labelPosition", value)}>
<SelectTrigger className="mt-1 h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}>
<SelectTrigger className="mt-1 h-6 w-full px-2 py-0 text-xs">
<SelectValue placeholder="라벨 위치 선택" />
</SelectTrigger>
<SelectContent>
@@ -194,18 +194,18 @@ export const CheckboxTypeConfigPanel: React.FC<CheckboxTypeConfigPanelProps> = (
<Label className="text-sm font-medium text-gray-700"></Label>
<div className="mt-2 flex items-center space-x-2">
{localValues.labelPosition === "left" && localValues.checkboxText && (
<span className="text-xs" style={{ fontSize: "12px" }}>{localValues.checkboxText}</span>
<span className="text-xs">{localValues.checkboxText}</span>
)}
{localValues.labelPosition === "top" && localValues.checkboxText && (
<div className="w-full">
<div className="text-xs" style={{ fontSize: "12px" }}>{localValues.checkboxText}</div>
<div className="text-xs">{localValues.checkboxText}</div>
<Checkbox checked={localValues.defaultChecked} className="mt-1" />
</div>
)}
{(localValues.labelPosition === "right" || localValues.labelPosition === "bottom") && (
<>
<Checkbox checked={localValues.defaultChecked} />
{localValues.checkboxText && <span className="text-xs" style={{ fontSize: "12px" }}>{localValues.checkboxText}</span>}
{localValues.checkboxText && <span className="text-xs">{localValues.checkboxText}</span>}
</>
)}
{localValues.labelPosition === "left" && <Checkbox checked={localValues.defaultChecked} />}

View File

@@ -121,7 +121,7 @@ export const CodeTypeConfigPanel: React.FC<CodeTypeConfigPanelProps> = ({ config
</Label>
<Select value={localValues.language} onValueChange={(value) => updateConfig("language", value)}>
<SelectTrigger className="mt-1 h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}>
<SelectTrigger className="mt-1 h-6 w-full px-2 py-0 text-xs">
<SelectValue placeholder="언어 선택" />
</SelectTrigger>
<SelectContent className="max-h-60">
@@ -140,7 +140,7 @@ export const CodeTypeConfigPanel: React.FC<CodeTypeConfigPanelProps> = ({ config
</Label>
<Select value={localValues.theme} onValueChange={(value) => updateConfig("theme", value)}>
<SelectTrigger className="mt-1 h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}>
<SelectTrigger className="mt-1 h-6 w-full px-2 py-0 text-xs">
<SelectValue placeholder="테마 선택" />
</SelectTrigger>
<SelectContent>

View File

@@ -193,7 +193,7 @@ export const DateTypeConfigPanel: React.FC<DateTypeConfigPanelProps> = ({ config
}, 0);
}}
>
<SelectTrigger className="mt-1 h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}>
<SelectTrigger className="mt-1 h-6 w-full px-2 py-0 text-xs">
<SelectValue placeholder="날짜 형식 선택" />
</SelectTrigger>
<SelectContent>

View File

@@ -233,7 +233,7 @@ export const EntityTypeConfigPanel: React.FC<EntityTypeConfigPanelProps> = ({ co
</Label>
<Select value={localValues.displayFormat} onValueChange={(value) => updateConfig("displayFormat", value)}>
<SelectTrigger className="mt-1 h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}>
<SelectTrigger className="mt-1 h-6 w-full px-2 py-0 text-xs">
<SelectValue placeholder="형식 선택" />
</SelectTrigger>
<SelectContent>
@@ -267,7 +267,7 @@ export const EntityTypeConfigPanel: React.FC<EntityTypeConfigPanelProps> = ({ co
{/* 기존 필터 목록 */}
<div className="max-h-40 space-y-2 overflow-y-auto">
{Object.entries(safeConfig.filters || {}).map(([field, value]) => (
<div key={field} className="flex items-center space-x-2 rounded border p-2 text-xs" style={{ fontSize: "12px" }}>
<div key={field} className="flex items-center space-x-2 rounded border p-2 text-xs">
<Input
value={field}
onChange={(e) => updateFilter(field, e.target.value, value as string)}
@@ -317,7 +317,7 @@ export const EntityTypeConfigPanel: React.FC<EntityTypeConfigPanelProps> = ({ co
<div className="mt-2">
<div className="flex items-center space-x-2 rounded border bg-white p-2">
<Search className="h-4 w-4 text-gray-400" />
<div className="text-muted-foreground flex-1 text-xs" style={{ fontSize: "12px" }}>
<div className="text-muted-foreground flex-1 text-xs">
{localValues.placeholder || `${localValues.referenceTable || "엔터티"}를 선택하세요`}
</div>
<Database className="h-4 w-4 text-gray-400" />

View File

@@ -111,7 +111,7 @@ export const NumberTypeConfigPanel: React.FC<NumberTypeConfigPanelProps> = ({ co
</Label>
<Select value={localValues.format} onValueChange={(value) => updateConfig("format", value)}>
<SelectTrigger className="mt-1 h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}>
<SelectTrigger className="mt-1 h-6 w-full px-2 py-0 text-xs">
<SelectValue placeholder="숫자 형식 선택" />
</SelectTrigger>
<SelectContent>

View File

@@ -259,7 +259,7 @@ export const RadioTypeConfigPanel: React.FC<RadioTypeConfigPanelProps> = ({ conf
{(safeConfig.options || []).map((option) => (
<div key={option.value} className="flex items-center space-x-2">
<RadioGroupItem value={option.value} id={`preview-${option.value}`} />
<Label htmlFor={`preview-${option.value}`} className="text-xs" style={{ fontSize: "12px" }}>
<Label htmlFor={`preview-${option.value}`} className="text-xs">
{option.label}
</Label>
</div>

View File

@@ -170,7 +170,7 @@ export const SelectTypeConfigPanel: React.FC<SelectTypeConfigPanelProps> = ({ co
value={localValues.placeholder}
onChange={(e) => updateConfig("placeholder", e.target.value)}
placeholder="옵션을 선택하세요"
className="mt-1 h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }} style={{ fontSize: "12px" }}
className="mt-1 h-6 w-full px-2 py-0 text-xs"
/>
</div>

View File

@@ -218,7 +218,7 @@ export const TextTypeConfigPanel: React.FC<TextTypeConfigPanelProps> = ({
</Label>
<Select value={localValues.format} onValueChange={(value) => updateConfig("format", value)}>
<SelectTrigger className="mt-1 h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}>
<SelectTrigger className="mt-1 h-6 w-full px-2 py-0 text-xs">
<SelectValue placeholder="입력 형식 선택" />
</SelectTrigger>
<SelectContent>
@@ -332,7 +332,7 @@ export const TextTypeConfigPanel: React.FC<TextTypeConfigPanelProps> = ({
</Label>
<Select value={localValues.autoValueType} onValueChange={(value) => updateConfig("autoValueType", value)}>
<SelectTrigger className="mt-1 h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}>
<SelectTrigger className="mt-1 h-6 w-full px-2 py-0 text-xs">
<SelectValue placeholder="자동값 타입 선택" />
</SelectTrigger>
<SelectContent>

View File

@@ -202,7 +202,7 @@ export const TextareaTypeConfigPanel: React.FC<TextareaTypeConfigPanelProps> = (
<Label className="text-sm font-medium text-gray-700"></Label>
<div className="mt-2">
<textarea
className="w-full rounded border border-gray-300 p-2 text-xs" style={{ fontSize: "12px" }}
className="w-full rounded border border-gray-300 p-2 text-xs"
rows={localValues.rows}
placeholder={localValues.placeholder || "텍스트를 입력하세요..."}
style={{

View File

@@ -1194,7 +1194,7 @@ export function FlowWidget({
setStepDataPage(1); // 페이지 크기 변경 시 첫 페이지로
}}
>
<SelectTrigger className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}>
<SelectTrigger className="h-6 w-full px-2 py-0 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>

View File

@@ -34,7 +34,7 @@ export default function InputWidget({ widget, value, onChange, className }: Inpu
required={widget.required}
readOnly={widget.readonly}
className={cn("h-6 w-full text-xs", widget.readonly && "bg-muted/50 cursor-not-allowed")}
style={{ fontSize: "12px" }}
className="text-xs"
/>
</div>
);

View File

@@ -53,7 +53,7 @@ export default function SelectWidget({ widget, value, onChange, options = [], cl
</Label>
)}
<Select value={value} onValueChange={handleChange} disabled={widget.readonly}>
<SelectTrigger className="h-6 w-full px-2 py-0 text-xs" style={{ fontSize: "12px" }}>
<SelectTrigger className="h-6 w-full px-2 py-0 text-xs">
<SelectValue placeholder={widget.placeholder || "선택해주세요"} />
</SelectTrigger>
<SelectContent>

View File

@@ -1,210 +1,258 @@
"use client";
import React, { useState, useEffect } from "react";
import { TabsComponent, TabItem, ScreenDefinition } from "@/types";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Card } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Loader2, FileQuestion } from "lucide-react";
import { screenApi } from "@/lib/api/screen";
import { Button } from "@/components/ui/button";
import { X, Loader2 } from "lucide-react";
import type { TabsComponent, TabItem } from "@/types/screen-management";
import { InteractiveScreenViewerDynamic } from "@/components/screen/InteractiveScreenViewerDynamic";
interface TabsWidgetProps {
component: TabsComponent;
isPreview?: boolean;
className?: string;
style?: React.CSSProperties;
}
/**
* 탭 위젯 컴포넌트
* 각 탭에 다른 화면을 표시할 수 있습니다
*/
export const TabsWidget: React.FC<TabsWidgetProps> = ({ component, isPreview = false }) => {
// componentConfig에서 설정 읽기 (새 컴포넌트 시스템)
const config = (component as any).componentConfig || component;
const { tabs = [], defaultTab, orientation = "horizontal", variant = "default" } = config;
// console.log("🔍 TabsWidget 렌더링:", {
// component,
// componentConfig: (component as any).componentConfig,
// tabs,
// tabsLength: tabs.length
// });
export function TabsWidget({ component, className, style }: TabsWidgetProps) {
const {
tabs = [],
defaultTab,
orientation = "horizontal",
variant = "default",
allowCloseable = false,
persistSelection = false,
} = component;
const [activeTab, setActiveTab] = useState<string>(defaultTab || tabs[0]?.id || "");
const [loadedScreens, setLoadedScreens] = useState<Record<string, any>>({});
console.log("🎨 TabsWidget 렌더링:", {
componentId: component.id,
tabs,
tabsLength: tabs.length,
component,
});
const storageKey = `tabs-${component.id}-selected`;
// 초기 선택 탭 결정
const getInitialTab = () => {
if (persistSelection && typeof window !== "undefined") {
const saved = localStorage.getItem(storageKey);
if (saved && tabs.some((t) => t.id === saved)) {
return saved;
}
}
return defaultTab || tabs[0]?.id || "";
};
const [selectedTab, setSelectedTab] = useState<string>(getInitialTab());
const [visibleTabs, setVisibleTabs] = useState<TabItem[]>(tabs);
const [loadingScreens, setLoadingScreens] = useState<Record<string, boolean>>({});
const [screenErrors, setScreenErrors] = useState<Record<string, string>>({});
const [screenLayouts, setScreenLayouts] = useState<Record<number, any>>({});
// 탭 변경 시 화면 로드
// 컴포넌트 탭 목록 변경 시 동기화
useEffect(() => {
if (!activeTab) return;
setVisibleTabs(tabs.filter((tab) => !tab.disabled));
}, [tabs]);
const currentTab = tabs.find((tab) => tab.id === activeTab);
if (!currentTab || !currentTab.screenId) return;
// 선택된 탭 변경 시 localStorage에 저장
useEffect(() => {
if (persistSelection && typeof window !== "undefined") {
localStorage.setItem(storageKey, selectedTab);
}
}, [selectedTab, persistSelection, storageKey]);
// 이미 로드된 화면이면 스킵
if (loadedScreens[activeTab]) return;
// 초기 로드 시 선택된 탭의 화면 불러오기
useEffect(() => {
const currentTab = visibleTabs.find((t) => t.id === selectedTab);
console.log("🔄 초기 탭 로드:", {
selectedTab,
currentTab,
hasScreenId: !!currentTab?.screenId,
screenId: currentTab?.screenId,
});
if (currentTab && currentTab.screenId && !screenLayouts[currentTab.screenId]) {
console.log("📥 초기 화면 로딩 시작:", currentTab.screenId);
loadScreenLayout(currentTab.screenId);
}
}, [selectedTab, visibleTabs]);
// 이미 로딩 중이면 스킵
if (loadingScreens[activeTab]) return;
// 화면 레이아웃 로드
const loadScreenLayout = async (screenId: number) => {
if (screenLayouts[screenId]) {
console.log("✅ 이미 로드된 화면:", screenId);
return; // 이미 로드됨
}
// 화면 로드 시작
loadScreen(activeTab, currentTab.screenId);
}, [activeTab, tabs]);
const loadScreen = async (tabId: string, screenId: number) => {
setLoadingScreens((prev) => ({ ...prev, [tabId]: true }));
setScreenErrors((prev) => ({ ...prev, [tabId]: "" }));
console.log("📥 화면 레이아웃 로딩 시작:", screenId);
setLoadingScreens((prev) => ({ ...prev, [screenId]: true }));
try {
const layoutData = await screenApi.getLayout(screenId);
if (layoutData) {
setLoadedScreens((prev) => ({
...prev,
[tabId]: {
screenId,
layout: layoutData,
},
}));
} else {
setScreenErrors((prev) => ({
...prev,
[tabId]: "화면을 불러올 수 없습니다",
}));
}
} catch (error: any) {
setScreenErrors((prev) => ({
...prev,
[tabId]: error.message || "화면 로드 중 오류가 발생했습니다",
}));
} finally {
setLoadingScreens((prev) => ({ ...prev, [tabId]: false }));
}
};
// 탭 콘텐츠 렌더링
const renderTabContent = (tab: TabItem) => {
const isLoading = loadingScreens[tab.id];
const error = screenErrors[tab.id];
const screenData = loadedScreens[tab.id];
// 로딩 중
if (isLoading) {
return (
<div className="flex h-full flex-col items-center justify-center space-y-4">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
<p className="text-muted-foreground text-sm"> ...</p>
</div>
);
}
// 에러 발생
if (error) {
return (
<div className="flex h-full flex-col items-center justify-center space-y-4">
<FileQuestion className="h-12 w-12 text-destructive" />
<div className="text-center">
<p className="mb-2 font-medium text-destructive"> </p>
<p className="text-muted-foreground text-sm">{error}</p>
</div>
</div>
);
}
// 화면 ID가 없는 경우
if (!tab.screenId) {
return (
<div className="flex h-full flex-col items-center justify-center space-y-4">
<FileQuestion className="text-muted-foreground h-12 w-12" />
<div className="text-center">
<p className="text-muted-foreground mb-2 text-sm"> </p>
<p className="text-xs text-gray-400"> </p>
</div>
</div>
);
}
// 화면 렌더링 - 원본 화면의 모든 컴포넌트를 그대로 렌더링
if (screenData && screenData.layout && screenData.layout.components) {
const components = screenData.layout.components;
const screenResolution = screenData.layout.screenResolution || { width: 1920, height: 1080 };
const { apiClient } = await import("@/lib/api/client");
const response = await apiClient.get(`/screen-management/screens/${screenId}/layout`);
console.log("📦 API 응답:", { screenId, success: response.data.success, hasData: !!response.data.data });
return (
<div className="bg-white" style={{ width: `${screenResolution.width}px`, height: '100%' }}>
<div className="relative h-full">
{components.map((comp) => (
<InteractiveScreenViewerDynamic
key={comp.id}
component={comp}
allComponents={components}
screenInfo={{ id: tab.screenId }}
/>
))}
</div>
</div>
);
if (response.data.success && response.data.data) {
console.log("✅ 화면 레이아웃 로드 완료:", screenId);
setScreenLayouts((prev) => ({ ...prev, [screenId]: response.data.data }));
} else {
console.error("❌ 화면 레이아웃 로드 실패 - success false");
}
} catch (error) {
console.error(`❌ 화면 레이아웃 로드 실패 ${screenId}:`, error);
} finally {
setLoadingScreens((prev) => ({ ...prev, [screenId]: false }));
}
return (
<div className="flex h-full flex-col items-center justify-center space-y-4">
<FileQuestion className="text-muted-foreground h-12 w-12" />
<div className="text-center">
<p className="text-muted-foreground text-sm"> </p>
</div>
</div>
);
};
// 빈 탭 목록
if (tabs.length === 0) {
// 탭 변경 핸들러
const handleTabChange = (tabId: string) => {
console.log("🔄 탭 변경:", tabId);
setSelectedTab(tabId);
// 해당 탭의 화면 로드
const tab = visibleTabs.find((t) => t.id === tabId);
console.log("🔍 선택된 탭 정보:", { tab, hasScreenId: !!tab?.screenId, screenId: tab?.screenId });
if (tab && tab.screenId && !screenLayouts[tab.screenId]) {
console.log("📥 탭 변경 시 화면 로딩:", tab.screenId);
loadScreenLayout(tab.screenId);
}
};
// 탭 닫기 핸들러
const handleCloseTab = (tabId: string, e: React.MouseEvent) => {
e.stopPropagation();
const updatedTabs = visibleTabs.filter((tab) => tab.id !== tabId);
setVisibleTabs(updatedTabs);
// 닫은 탭이 선택된 탭이었다면 다음 탭 선택
if (selectedTab === tabId && updatedTabs.length > 0) {
setSelectedTab(updatedTabs[0].id);
}
};
// 탭 스타일 클래스
const getTabsListClass = () => {
const baseClass = orientation === "vertical" ? "flex-col" : "";
const variantClass =
variant === "pills"
? "bg-muted p-1 rounded-lg"
: variant === "underline"
? "border-b"
: "bg-muted p-1";
return `${baseClass} ${variantClass}`;
};
if (visibleTabs.length === 0) {
console.log("⚠️ 보이는 탭이 없음");
return (
<Card className="flex h-full w-full items-center justify-center">
<div className="text-center">
<p className="text-muted-foreground text-sm"> </p>
<p className="text-xs text-gray-400"> </p>
</div>
</Card>
<div className="flex h-full w-full items-center justify-center rounded border-2 border-dashed border-gray-300 bg-gray-50">
<p className="text-muted-foreground text-sm"> </p>
</div>
);
}
console.log("🎨 TabsWidget 최종 렌더링:", {
visibleTabsCount: visibleTabs.length,
selectedTab,
screenLayoutsKeys: Object.keys(screenLayouts),
loadingScreensKeys: Object.keys(loadingScreens),
});
return (
<div className="h-full w-full overflow-auto">
<div className="flex h-full w-full flex-col pt-4" style={style}>
<Tabs
value={activeTab}
onValueChange={setActiveTab}
value={selectedTab}
onValueChange={handleTabChange}
orientation={orientation}
className="flex h-full w-full flex-col"
>
<TabsList className={orientation === "horizontal" ? "justify-start shrink-0" : "flex-col shrink-0"}>
{tabs.map((tab) => (
<TabsTrigger
key={tab.id}
value={tab.id}
disabled={tab.disabled}
className={orientation === "horizontal" ? "" : "w-full justify-start"}
>
<span>{tab.label}</span>
{tab.screenName && (
<Badge variant="secondary" className="ml-2 text-[10px]">
{tab.screenName}
</Badge>
)}
</TabsTrigger>
))}
</TabsList>
<div className="relative z-10">
<TabsList className={getTabsListClass()}>
{visibleTabs.map((tab) => (
<div key={tab.id} className="relative">
<TabsTrigger value={tab.id} disabled={tab.disabled} className="relative pr-8">
{tab.label}
</TabsTrigger>
{allowCloseable && (
<Button
onClick={(e) => handleCloseTab(tab.id, e)}
variant="ghost"
size="sm"
className="absolute right-1 top-1/2 h-5 w-5 -translate-y-1/2 p-0 hover:bg-destructive/10"
>
<X className="h-3 w-3" />
</Button>
)}
</div>
))}
</TabsList>
</div>
{tabs.map((tab) => (
<TabsContent
key={tab.id}
value={tab.id}
className="flex-1 mt-0 data-[state=inactive]:hidden"
>
{renderTabContent(tab)}
</TabsContent>
))}
<div className="relative flex-1 overflow-hidden">
{visibleTabs.map((tab) => (
<TabsContent key={tab.id} value={tab.id} className="h-full">
{tab.screenId ? (
loadingScreens[tab.screenId] ? (
<div className="flex h-full w-full items-center justify-center">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
<span className="text-muted-foreground ml-2"> ...</span>
</div>
) : screenLayouts[tab.screenId] ? (
(() => {
const layoutData = screenLayouts[tab.screenId];
const { components = [], screenResolution } = layoutData;
console.log("🎯 렌더링할 화면 데이터:", {
screenId: tab.screenId,
componentsCount: components.length,
screenResolution,
});
const designWidth = screenResolution?.width || 1920;
const designHeight = screenResolution?.height || 1080;
return (
<div
className="relative h-full w-full overflow-auto bg-background"
style={{
minHeight: `${designHeight}px`,
}}
>
<div
className="relative"
style={{
width: `${designWidth}px`,
height: `${designHeight}px`,
margin: "0 auto",
}}
>
{components.map((component: any) => (
<InteractiveScreenViewerDynamic
key={component.id}
component={component}
allComponents={components}
/>
))}
</div>
</div>
);
})()
) : (
<div className="flex h-full w-full items-center justify-center">
<p className="text-muted-foreground text-sm"> </p>
</div>
)
) : (
<div className="flex h-full w-full items-center justify-center rounded border-2 border-dashed border-gray-300 bg-gray-50">
<p className="text-muted-foreground text-sm"> </p>
</div>
)}
</TabsContent>
))}
</div>
</Tabs>
</div>
);
};
}