Compare commits
2 Commits
9d74baf60a
...
6ea3aef396
| Author | SHA1 | Date |
|---|---|---|
|
|
6ea3aef396 | |
|
|
b4bfb9964f |
|
|
@ -489,8 +489,73 @@ function ScreenViewPage() {
|
|||
(c as any).componentType === "conditional-container",
|
||||
);
|
||||
|
||||
// TableSearchWidget 및 조건부 컨테이너 높이 차이를 계산하여 Y 위치 조정
|
||||
const adjustedComponents = regularComponents.map((component) => {
|
||||
// 🆕 같은 X 영역(섹션)에서 컴포넌트들이 겹치지 않도록 자동 수직 정렬
|
||||
const autoLayoutComponents = (() => {
|
||||
// X 위치 기준으로 섹션 그룹화 (50px 오차 범위)
|
||||
const X_THRESHOLD = 50;
|
||||
const GAP = 16; // 컴포넌트 간 간격
|
||||
|
||||
// 컴포넌트를 X 섹션별로 그룹화
|
||||
const sections: Map<number, typeof regularComponents> = new Map();
|
||||
|
||||
regularComponents.forEach((comp) => {
|
||||
const x = comp.position.x;
|
||||
let foundSection = false;
|
||||
|
||||
for (const [sectionX, components] of sections.entries()) {
|
||||
if (Math.abs(x - sectionX) < X_THRESHOLD) {
|
||||
components.push(comp);
|
||||
foundSection = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundSection) {
|
||||
sections.set(x, [comp]);
|
||||
}
|
||||
});
|
||||
|
||||
// 각 섹션 내에서 Y 위치 순으로 정렬 후 자동 배치
|
||||
const adjustedMap = new Map<string, typeof regularComponents[0]>();
|
||||
|
||||
for (const [sectionX, components] of sections.entries()) {
|
||||
// 섹션 내 2개 이상 컴포넌트가 있을 때만 자동 배치
|
||||
if (components.length >= 2) {
|
||||
// Y 위치 순으로 정렬
|
||||
const sorted = [...components].sort((a, b) => a.position.y - b.position.y);
|
||||
|
||||
let currentY = sorted[0].position.y;
|
||||
|
||||
sorted.forEach((comp, index) => {
|
||||
if (index === 0) {
|
||||
adjustedMap.set(comp.id, comp);
|
||||
} else {
|
||||
// 이전 컴포넌트 아래로 배치
|
||||
const prevComp = sorted[index - 1];
|
||||
const prevAdjusted = adjustedMap.get(prevComp.id) || prevComp;
|
||||
const prevBottom = prevAdjusted.position.y + (prevAdjusted.size?.height || 100);
|
||||
const newY = prevBottom + GAP;
|
||||
|
||||
adjustedMap.set(comp.id, {
|
||||
...comp,
|
||||
position: {
|
||||
...comp.position,
|
||||
y: newY,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// 단일 컴포넌트는 그대로
|
||||
components.forEach((comp) => adjustedMap.set(comp.id, comp));
|
||||
}
|
||||
}
|
||||
|
||||
return regularComponents.map((comp) => adjustedMap.get(comp.id) || comp);
|
||||
})();
|
||||
|
||||
// TableSearchWidget 및 조건부 컨테이너 높이 차이를 계산하여 Y 위치 추가 조정
|
||||
const adjustedComponents = autoLayoutComponents.map((component) => {
|
||||
const isTableSearchWidget = (component as any).componentId === "table-search-widget";
|
||||
const isConditionalContainer = (component as any).componentId === "conditional-container";
|
||||
|
||||
|
|
@ -511,30 +576,15 @@ function ScreenViewPage() {
|
|||
}
|
||||
}
|
||||
|
||||
// 🆕 조건부 컨테이너 높이 조정
|
||||
// 조건부 컨테이너 높이 조정
|
||||
for (const container of conditionalContainers) {
|
||||
const isBelow = component.position.y > container.position.y;
|
||||
const actualHeight = conditionalContainerHeights[container.id];
|
||||
const originalHeight = container.size?.height || 200;
|
||||
const heightDiff = actualHeight ? actualHeight - originalHeight : 0;
|
||||
|
||||
console.log(`🔍 높이 조정 체크:`, {
|
||||
componentId: component.id,
|
||||
componentY: component.position.y,
|
||||
containerY: container.position.y,
|
||||
isBelow,
|
||||
actualHeight,
|
||||
originalHeight,
|
||||
heightDiff,
|
||||
containerId: container.id,
|
||||
containerSize: container.size,
|
||||
});
|
||||
|
||||
if (isBelow && heightDiff > 0) {
|
||||
totalHeightAdjustment += heightDiff;
|
||||
console.log(
|
||||
`📐 컴포넌트 ${component.id} 위치 조정: ${heightDiff}px (조건부 컨테이너 ${container.id})`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -307,12 +307,15 @@ export function RepeatContainerComponent({
|
|||
return {
|
||||
minWidth: itemMinWidth,
|
||||
maxWidth: itemMaxWidth,
|
||||
height: itemHeight,
|
||||
// height 대신 minHeight 사용 - 내부 컨텐츠가 커지면 자동으로 높이 확장
|
||||
minHeight: itemHeight || "auto",
|
||||
height: "auto", // 고정 높이 대신 auto로 변경
|
||||
backgroundColor: backgroundColor || "#ffffff",
|
||||
borderRadius: borderRadius || "8px",
|
||||
padding: padding || "16px",
|
||||
border: showBorder ? "1px solid #e5e7eb" : "none",
|
||||
boxShadow: showShadow ? "0 1px 3px rgba(0,0,0,0.1)" : "none",
|
||||
overflow: "visible", // 내부 컨텐츠가 튀어나가지 않도록
|
||||
};
|
||||
}, [itemMinWidth, itemMaxWidth, itemHeight, backgroundColor, borderRadius, padding, showBorder, showShadow]);
|
||||
|
||||
|
|
@ -343,11 +346,11 @@ export function RepeatContainerComponent({
|
|||
_isLast: context.isLast,
|
||||
};
|
||||
|
||||
// 슬롯에 배치된 컴포넌트들을 렌더링
|
||||
// 슬롯에 배치된 컴포넌트들을 렌더링 (Flow 레이아웃으로 변경)
|
||||
return (
|
||||
<div className="relative" style={{ minHeight: "50px" }}>
|
||||
<div className="flex flex-col gap-3">
|
||||
{slotChildren.map((childComp: SlotComponentConfig) => {
|
||||
const { position = { x: 0, y: 0 }, size = { width: 200, height: 40 } } = childComp;
|
||||
const { size = { width: "100%", height: "auto" } } = childComp;
|
||||
|
||||
// DynamicComponentRenderer가 기대하는 형식으로 변환
|
||||
const componentData = {
|
||||
|
|
@ -355,8 +358,11 @@ export function RepeatContainerComponent({
|
|||
componentType: childComp.componentType,
|
||||
label: childComp.label,
|
||||
columnName: childComp.fieldName,
|
||||
position: { ...position, z: 1 },
|
||||
size,
|
||||
position: { x: 0, y: 0, z: 1 },
|
||||
size: {
|
||||
width: typeof size.width === "number" ? size.width : undefined,
|
||||
height: typeof size.height === "number" ? size.height : undefined,
|
||||
},
|
||||
componentConfig: childComp.componentConfig,
|
||||
style: childComp.style,
|
||||
};
|
||||
|
|
@ -364,12 +370,10 @@ export function RepeatContainerComponent({
|
|||
return (
|
||||
<div
|
||||
key={componentData.id}
|
||||
className="absolute"
|
||||
className="w-full"
|
||||
style={{
|
||||
left: position.x || 0,
|
||||
top: position.y || 0,
|
||||
width: size.width || 200,
|
||||
height: size.height || 40,
|
||||
// 너비는 100%로, 높이는 자동으로
|
||||
minHeight: typeof size.height === "number" ? size.height : "auto",
|
||||
}}
|
||||
>
|
||||
<DynamicComponentRenderer
|
||||
|
|
|
|||
|
|
@ -14,11 +14,12 @@ import {
|
|||
} from "@/components/ui/select";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
|
||||
import { Database, Table2, ChevronsUpDown, Check, LayoutGrid, LayoutList, Rows3, Plus, X, GripVertical, Trash2, Type } from "lucide-react";
|
||||
import { Database, Table2, ChevronsUpDown, Check, LayoutGrid, LayoutList, Rows3, Plus, X, GripVertical, Trash2, Type, Settings2, ChevronDown, ChevronUp } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { RepeatContainerConfig, SlotComponentConfig } from "./types";
|
||||
import { tableTypeApi } from "@/lib/api/screen";
|
||||
import { tableManagementApi } from "@/lib/api/tableManagement";
|
||||
import { DynamicComponentConfigPanel, hasComponentConfigPanel } from "@/lib/utils/getComponentConfigPanel";
|
||||
|
||||
interface RepeatContainerConfigPanelProps {
|
||||
config: RepeatContainerConfig;
|
||||
|
|
@ -263,6 +264,7 @@ export function RepeatContainerConfigPanel({
|
|||
onChange={onChange}
|
||||
availableColumns={availableColumns}
|
||||
loadingColumns={loadingColumns}
|
||||
screenTableName={screenTableName}
|
||||
/>
|
||||
|
||||
{/* 레이아웃 설정 */}
|
||||
|
|
@ -597,11 +599,89 @@ export function RepeatContainerConfigPanel({
|
|||
// ============================================================
|
||||
// 슬롯 자식 컴포넌트 관리 섹션
|
||||
// ============================================================
|
||||
|
||||
// 슬롯 컴포넌트의 전체 설정 패널을 표시하는 컴포넌트
|
||||
interface SlotComponentDetailPanelProps {
|
||||
child: SlotComponentConfig;
|
||||
screenTableName?: string;
|
||||
availableColumns: Array<{ columnName: string; displayName?: string }>;
|
||||
onConfigChange: (newConfig: Record<string, any>) => void;
|
||||
onFieldNameChange: (fieldName: string) => void;
|
||||
onLabelChange: (label: string) => void;
|
||||
}
|
||||
|
||||
function SlotComponentDetailPanel({
|
||||
child,
|
||||
screenTableName,
|
||||
availableColumns,
|
||||
onConfigChange,
|
||||
onFieldNameChange,
|
||||
onLabelChange,
|
||||
}: SlotComponentDetailPanelProps) {
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{/* 데이터 필드 바인딩 - 모든 컴포넌트에서 사용 가능 */}
|
||||
<div className="space-y-1 p-2 bg-blue-50 rounded-md border border-blue-200">
|
||||
<Label className="text-[10px] text-blue-700 font-medium flex items-center gap-1">
|
||||
<Database className="h-3 w-3" />
|
||||
데이터 필드 바인딩
|
||||
</Label>
|
||||
<Select
|
||||
value={child.fieldName || ""}
|
||||
onValueChange={onFieldNameChange}
|
||||
>
|
||||
<SelectTrigger className="h-7 text-xs bg-white">
|
||||
<SelectValue placeholder="표시할 필드 선택..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="">선택 안함</SelectItem>
|
||||
{availableColumns.map((col) => (
|
||||
<SelectItem key={col.columnName} value={col.columnName}>
|
||||
{col.displayName || col.columnName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{child.fieldName && (
|
||||
<p className="text-[9px] text-blue-600">
|
||||
각 아이템의 "{child.fieldName}" 값이 이 컴포넌트에 표시됩니다
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 라벨 설정 */}
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[10px] text-slate-500">표시 라벨</Label>
|
||||
<Input
|
||||
value={child.label || ""}
|
||||
onChange={(e) => onLabelChange(e.target.value)}
|
||||
placeholder="표시할 라벨"
|
||||
className="h-7 text-xs"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 컴포넌트 전용 설정 */}
|
||||
<div className="border-t pt-2">
|
||||
<div className="text-[10px] font-medium text-slate-600 mb-2">
|
||||
{child.componentType} 상세 설정
|
||||
</div>
|
||||
<DynamicComponentConfigPanel
|
||||
componentId={child.componentType}
|
||||
config={child.componentConfig || {}}
|
||||
onChange={onConfigChange}
|
||||
screenTableName={screenTableName}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface SlotChildrenSectionProps {
|
||||
config: RepeatContainerConfig;
|
||||
onChange: (config: Partial<RepeatContainerConfig>) => void;
|
||||
availableColumns: Array<{ columnName: string; displayName?: string }>;
|
||||
loadingColumns: boolean;
|
||||
screenTableName?: string;
|
||||
}
|
||||
|
||||
function SlotChildrenSection({
|
||||
|
|
@ -609,12 +689,28 @@ function SlotChildrenSection({
|
|||
onChange,
|
||||
availableColumns,
|
||||
loadingColumns,
|
||||
screenTableName,
|
||||
}: SlotChildrenSectionProps) {
|
||||
const [selectedColumn, setSelectedColumn] = useState<string>("");
|
||||
const [columnComboboxOpen, setColumnComboboxOpen] = useState(false);
|
||||
// 각 컴포넌트별 상세 설정 열림 상태
|
||||
const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set());
|
||||
|
||||
const children = config.children || [];
|
||||
|
||||
// 상세 설정 열기/닫기 토글
|
||||
const toggleExpanded = (id: string) => {
|
||||
setExpandedIds((prev) => {
|
||||
const newSet = new Set(prev);
|
||||
if (newSet.has(id)) {
|
||||
newSet.delete(id);
|
||||
} else {
|
||||
newSet.add(id);
|
||||
}
|
||||
return newSet;
|
||||
});
|
||||
};
|
||||
|
||||
// 컴포넌트 추가
|
||||
const addComponent = (columnName: string, displayName: string) => {
|
||||
const newChild: SlotComponentConfig = {
|
||||
|
|
@ -625,6 +721,7 @@ function SlotChildrenSection({
|
|||
position: { x: 0, y: children.length * 40 },
|
||||
size: { width: 200, height: 32 },
|
||||
componentConfig: {},
|
||||
style: {},
|
||||
};
|
||||
|
||||
onChange({
|
||||
|
|
@ -639,6 +736,11 @@ function SlotChildrenSection({
|
|||
onChange({
|
||||
children: children.filter((c) => c.id !== id),
|
||||
});
|
||||
setExpandedIds((prev) => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.delete(id);
|
||||
return newSet;
|
||||
});
|
||||
};
|
||||
|
||||
// 컴포넌트 라벨 변경
|
||||
|
|
@ -655,6 +757,46 @@ function SlotChildrenSection({
|
|||
});
|
||||
};
|
||||
|
||||
// 컴포넌트 필드 바인딩 변경
|
||||
const updateComponentFieldName = (id: string, fieldName: string) => {
|
||||
onChange({
|
||||
children: children.map((c) => (c.id === id ? { ...c, fieldName } : c)),
|
||||
});
|
||||
};
|
||||
|
||||
// 컴포넌트 설정 변경 (componentConfig)
|
||||
const updateComponentConfig = (id: string, key: string, value: any) => {
|
||||
onChange({
|
||||
children: children.map((c) =>
|
||||
c.id === id
|
||||
? { ...c, componentConfig: { ...c.componentConfig, [key]: value } }
|
||||
: c
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
// 컴포넌트 스타일 변경
|
||||
const updateComponentStyle = (id: string, key: string, value: any) => {
|
||||
onChange({
|
||||
children: children.map((c) =>
|
||||
c.id === id
|
||||
? { ...c, style: { ...c.style, [key]: value } }
|
||||
: c
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
// 컴포넌트 크기 변경
|
||||
const updateComponentSize = (id: string, width: number | undefined, height: number | undefined) => {
|
||||
onChange({
|
||||
children: children.map((c) =>
|
||||
c.id === id
|
||||
? { ...c, size: { width: width ?? c.size?.width ?? 200, height: height ?? c.size?.height ?? 32 } }
|
||||
: c
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
|
|
@ -668,16 +810,18 @@ function SlotChildrenSection({
|
|||
{/* 추가된 필드 목록 */}
|
||||
{children.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
{children.map((child, index) => (
|
||||
<div
|
||||
key={child.id}
|
||||
className="flex items-center gap-2 rounded-md border border-green-200 bg-green-50 p-2"
|
||||
>
|
||||
<div className="flex h-5 w-5 items-center justify-center rounded bg-green-200 text-xs font-medium text-green-700">
|
||||
{index + 1}
|
||||
</div>
|
||||
<div className="flex-1 space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
{children.map((child, index) => {
|
||||
const isExpanded = expandedIds.has(child.id);
|
||||
return (
|
||||
<div
|
||||
key={child.id}
|
||||
className="rounded-md border border-green-200 bg-green-50 overflow-hidden"
|
||||
>
|
||||
{/* 기본 정보 헤더 */}
|
||||
<div className="flex items-center gap-2 p-2">
|
||||
<div className="flex h-5 w-5 items-center justify-center rounded bg-green-200 text-xs font-medium text-green-700">
|
||||
{index + 1}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="text-xs font-medium text-green-700">
|
||||
{child.label || child.fieldName}
|
||||
|
|
@ -700,18 +844,257 @@ function SlotChildrenSection({
|
|||
<SelectItem value="date-display">날짜</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-5 w-5 p-0 text-blue-500 hover:text-blue-700"
|
||||
onClick={() => toggleExpanded(child.id)}
|
||||
title="상세 설정"
|
||||
>
|
||||
{isExpanded ? (
|
||||
<ChevronUp className="h-3 w-3" />
|
||||
) : (
|
||||
<Settings2 className="h-3 w-3" />
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-5 w-5 p-0 text-red-400 hover:text-red-600"
|
||||
onClick={() => removeComponent(child.id)}
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 상세 설정 패널 (펼침) */}
|
||||
{isExpanded && (
|
||||
<div className="border-t border-green-200 bg-white p-3 space-y-3">
|
||||
{/* 전용 ConfigPanel이 있는 복잡한 컴포넌트인 경우 */}
|
||||
{hasComponentConfigPanel(child.componentType) ? (
|
||||
<SlotComponentDetailPanel
|
||||
child={child}
|
||||
screenTableName={screenTableName}
|
||||
availableColumns={availableColumns}
|
||||
onConfigChange={(newConfig) => {
|
||||
onChange({
|
||||
children: children.map((c) =>
|
||||
c.id === child.id
|
||||
? { ...c, componentConfig: { ...c.componentConfig, ...newConfig } }
|
||||
: c
|
||||
),
|
||||
});
|
||||
}}
|
||||
onFieldNameChange={(fieldName) => updateComponentFieldName(child.id, fieldName)}
|
||||
onLabelChange={(label) => updateComponentLabel(child.id, label)}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{/* 데이터 필드 바인딩 - 가장 중요! */}
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[10px] text-slate-500 font-medium flex items-center gap-1">
|
||||
<Database className="h-3 w-3 text-blue-500" />
|
||||
데이터 필드
|
||||
</Label>
|
||||
<Select
|
||||
value={child.fieldName || ""}
|
||||
onValueChange={(value) => updateComponentFieldName(child.id, value)}
|
||||
>
|
||||
<SelectTrigger className="h-7 text-xs">
|
||||
<SelectValue placeholder="표시할 필드 선택..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="">선택 안함</SelectItem>
|
||||
{availableColumns.map((col) => (
|
||||
<SelectItem key={col.columnName} value={col.columnName}>
|
||||
{col.displayName || col.columnName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{child.fieldName && (
|
||||
<p className="text-[9px] text-green-600">
|
||||
각 아이템의 "{child.fieldName}" 값이 표시됩니다
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 라벨 설정 */}
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[10px] text-slate-500">표시 라벨</Label>
|
||||
<Input
|
||||
value={child.label || ""}
|
||||
onChange={(e) => updateComponentLabel(child.id, e.target.value)}
|
||||
placeholder="표시할 라벨"
|
||||
className="h-7 text-xs"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 크기 설정 */}
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[10px] text-slate-500">너비 (px)</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={child.size?.width || 200}
|
||||
onChange={(e) =>
|
||||
updateComponentSize(child.id, parseInt(e.target.value) || 200, undefined)
|
||||
}
|
||||
className="h-7 text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[10px] text-slate-500">높이 (px)</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={child.size?.height || 32}
|
||||
onChange={(e) =>
|
||||
updateComponentSize(child.id, undefined, parseInt(e.target.value) || 32)
|
||||
}
|
||||
className="h-7 text-xs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 스타일 설정 */}
|
||||
<div className="space-y-2">
|
||||
<Label className="text-[10px] text-slate-500 font-medium">스타일</Label>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[10px] text-slate-400">글자 크기</Label>
|
||||
<Select
|
||||
value={child.style?.fontSize || "14px"}
|
||||
onValueChange={(value) => updateComponentStyle(child.id, "fontSize", value)}
|
||||
>
|
||||
<SelectTrigger className="h-7 text-xs">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="10px">10px (아주 작게)</SelectItem>
|
||||
<SelectItem value="12px">12px (작게)</SelectItem>
|
||||
<SelectItem value="14px">14px (보통)</SelectItem>
|
||||
<SelectItem value="16px">16px (크게)</SelectItem>
|
||||
<SelectItem value="18px">18px (아주 크게)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[10px] text-slate-400">글자 굵기</Label>
|
||||
<Select
|
||||
value={child.style?.fontWeight || "normal"}
|
||||
onValueChange={(value) => updateComponentStyle(child.id, "fontWeight", value)}
|
||||
>
|
||||
<SelectTrigger className="h-7 text-xs">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="normal">보통</SelectItem>
|
||||
<SelectItem value="500">중간</SelectItem>
|
||||
<SelectItem value="600">약간 굵게</SelectItem>
|
||||
<SelectItem value="bold">굵게</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[10px] text-slate-400">글자 정렬</Label>
|
||||
<Select
|
||||
value={child.style?.textAlign || "left"}
|
||||
onValueChange={(value) => updateComponentStyle(child.id, "textAlign", value)}
|
||||
>
|
||||
<SelectTrigger className="h-7 text-xs">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="left">왼쪽</SelectItem>
|
||||
<SelectItem value="center">가운데</SelectItem>
|
||||
<SelectItem value="right">오른쪽</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[10px] text-slate-400">글자 색상</Label>
|
||||
<div className="flex gap-1">
|
||||
<Input
|
||||
type="color"
|
||||
value={child.style?.color || "#000000"}
|
||||
onChange={(e) => updateComponentStyle(child.id, "color", e.target.value)}
|
||||
className="h-7 w-10 p-0.5 cursor-pointer"
|
||||
/>
|
||||
<Input
|
||||
value={child.style?.color || "#000000"}
|
||||
onChange={(e) => updateComponentStyle(child.id, "color", e.target.value)}
|
||||
className="h-7 flex-1 text-xs"
|
||||
placeholder="#000000"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 컴포넌트 타입별 추가 설정 */}
|
||||
{(child.componentType === "number-display" || child.componentType === "number-input") && (
|
||||
<div className="space-y-2">
|
||||
<Label className="text-[10px] text-slate-500 font-medium">숫자 형식</Label>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[10px] text-slate-400">소수점 자릿수</Label>
|
||||
<Input
|
||||
type="number"
|
||||
min={0}
|
||||
max={10}
|
||||
value={child.componentConfig?.decimalPlaces ?? 0}
|
||||
onChange={(e) =>
|
||||
updateComponentConfig(child.id, "decimalPlaces", parseInt(e.target.value) || 0)
|
||||
}
|
||||
className="h-7 text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 pt-4">
|
||||
<Checkbox
|
||||
id={`thousandSep_${child.id}`}
|
||||
checked={child.componentConfig?.thousandSeparator ?? true}
|
||||
onCheckedChange={(checked) =>
|
||||
updateComponentConfig(child.id, "thousandSeparator", checked)
|
||||
}
|
||||
/>
|
||||
<Label htmlFor={`thousandSep_${child.id}`} className="text-[10px]">
|
||||
천 단위 구분
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(child.componentType === "date-display" || child.componentType === "date-input") && (
|
||||
<div className="space-y-2">
|
||||
<Label className="text-[10px] text-slate-500 font-medium">날짜 형식</Label>
|
||||
<Select
|
||||
value={child.componentConfig?.dateFormat || "YYYY-MM-DD"}
|
||||
onValueChange={(value) => updateComponentConfig(child.id, "dateFormat", value)}
|
||||
>
|
||||
<SelectTrigger className="h-7 text-xs">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="YYYY-MM-DD">2024-01-15</SelectItem>
|
||||
<SelectItem value="YYYY.MM.DD">2024.01.15</SelectItem>
|
||||
<SelectItem value="YYYY/MM/DD">2024/01/15</SelectItem>
|
||||
<SelectItem value="MM/DD/YYYY">01/15/2024</SelectItem>
|
||||
<SelectItem value="DD-MM-YYYY">15-01-2024</SelectItem>
|
||||
<SelectItem value="YYYY-MM-DD HH:mm">2024-01-15 14:30</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-5 w-5 p-0 text-red-400 hover:text-red-600"
|
||||
onClick={() => removeComponent(child.id)}
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-md border border-dashed border-slate-300 bg-slate-50 p-4 text-center">
|
||||
|
|
|
|||
|
|
@ -5,7 +5,9 @@
|
|||
import React from "react";
|
||||
|
||||
// 컴포넌트별 ConfigPanel 동적 import 맵
|
||||
// 모든 ConfigPanel이 있는 컴포넌트를 여기에 등록해야 슬롯/중첩 컴포넌트에서 전용 설정 패널이 표시됨
|
||||
const CONFIG_PANEL_MAP: Record<string, () => Promise<any>> = {
|
||||
// ========== 기본 입력 컴포넌트 ==========
|
||||
"text-input": () => import("@/lib/registry/components/text-input/TextInputConfigPanel"),
|
||||
"number-input": () => import("@/lib/registry/components/number-input/NumberInputConfigPanel"),
|
||||
"date-input": () => import("@/lib/registry/components/date-input/DateInputConfigPanel"),
|
||||
|
|
@ -15,34 +17,60 @@ const CONFIG_PANEL_MAP: Record<string, () => Promise<any>> = {
|
|||
"radio-basic": () => import("@/lib/registry/components/radio-basic/RadioBasicConfigPanel"),
|
||||
"toggle-switch": () => import("@/lib/registry/components/toggle-switch/ToggleSwitchConfigPanel"),
|
||||
"file-upload": () => import("@/lib/registry/components/file-upload/FileUploadConfigPanel"),
|
||||
"button-primary": () => import("@/components/screen/config-panels/ButtonConfigPanel"),
|
||||
"text-display": () => import("@/lib/registry/components/text-display/TextDisplayConfigPanel"),
|
||||
"slider-basic": () => import("@/lib/registry/components/slider-basic/SliderBasicConfigPanel"),
|
||||
"test-input": () => import("@/lib/registry/components/test-input/TestInputConfigPanel"),
|
||||
|
||||
// ========== 버튼 ==========
|
||||
"button-primary": () => import("@/components/screen/config-panels/ButtonConfigPanel"),
|
||||
|
||||
// ========== 표시 컴포넌트 ==========
|
||||
"text-display": () => import("@/lib/registry/components/text-display/TextDisplayConfigPanel"),
|
||||
"image-display": () => import("@/lib/registry/components/image-display/ImageDisplayConfigPanel"),
|
||||
"divider-line": () => import("@/lib/registry/components/divider-line/DividerLineConfigPanel"),
|
||||
"image-widget": () => import("@/lib/registry/components/image-widget/ImageWidgetConfigPanel"),
|
||||
|
||||
// ========== 레이아웃/컨테이너 ==========
|
||||
"accordion-basic": () => import("@/lib/registry/components/accordion-basic/AccordionBasicConfigPanel"),
|
||||
"table-list": () => import("@/lib/registry/components/table-list/TableListConfigPanel"),
|
||||
"card-display": () => import("@/lib/registry/components/card-display/CardDisplayConfigPanel"),
|
||||
"split-panel-layout": () => import("@/lib/registry/components/split-panel-layout/SplitPanelLayoutConfigPanel"),
|
||||
"split-panel-layout2": () => import("@/lib/registry/components/split-panel-layout2/SplitPanelLayout2ConfigPanel"),
|
||||
"repeater-field-group": () => import("@/components/webtypes/config/RepeaterConfigPanel"),
|
||||
"flow-widget": () => import("@/components/screen/config-panels/FlowWidgetConfigPanel"),
|
||||
// 🆕 수주 등록 관련 컴포넌트들
|
||||
"autocomplete-search-input": () =>
|
||||
import("@/lib/registry/components/autocomplete-search-input/AutocompleteSearchInputConfigPanel"),
|
||||
"entity-search-input": () => import("@/lib/registry/components/entity-search-input/EntitySearchInputConfigPanel"),
|
||||
"modal-repeater-table": () => import("@/lib/registry/components/modal-repeater-table/ModalRepeaterTableConfigPanel"),
|
||||
// 🆕 조건부 컨테이너
|
||||
"conditional-container": () =>
|
||||
import("@/lib/registry/components/conditional-container/ConditionalContainerConfigPanel"),
|
||||
// 🆕 선택 항목 상세입력
|
||||
"selected-items-detail-input": () =>
|
||||
import("@/lib/registry/components/selected-items-detail-input/SelectedItemsDetailInputConfigPanel"),
|
||||
// 🆕 섹션 그룹화 레이아웃
|
||||
"section-card": () => import("@/lib/registry/components/section-card/SectionCardConfigPanel"),
|
||||
"section-paper": () => import("@/lib/registry/components/section-paper/SectionPaperConfigPanel"),
|
||||
// 🆕 탭 컴포넌트
|
||||
"split-panel-layout": () => import("@/lib/registry/components/split-panel-layout/SplitPanelLayoutConfigPanel"),
|
||||
"split-panel-layout2": () => import("@/lib/registry/components/split-panel-layout2/SplitPanelLayout2ConfigPanel"),
|
||||
"screen-split-panel": () => import("@/lib/registry/components/screen-split-panel/ScreenSplitPanelConfigPanel"),
|
||||
"conditional-container": () => import("@/lib/registry/components/conditional-container/ConditionalContainerConfigPanel"),
|
||||
|
||||
// ========== 테이블/리스트 ==========
|
||||
"table-list": () => import("@/lib/registry/components/table-list/TableListConfigPanel"),
|
||||
"pivot-grid": () => import("@/lib/registry/components/pivot-grid/PivotGridConfigPanel"),
|
||||
"table-search-widget": () => import("@/lib/registry/components/table-search-widget/TableSearchWidgetConfigPanel"),
|
||||
"tax-invoice-list": () => import("@/lib/registry/components/tax-invoice-list/TaxInvoiceListConfigPanel"),
|
||||
|
||||
// ========== 리피터/반복 ==========
|
||||
"repeat-container": () => import("@/lib/registry/components/repeat-container/RepeatContainerConfigPanel"),
|
||||
"repeater-field-group": () => import("@/components/webtypes/config/RepeaterConfigPanel"),
|
||||
"unified-repeater": () => import("@/components/unified/config-panels/UnifiedRepeaterConfigPanel"),
|
||||
"simple-repeater-table": () => import("@/lib/registry/components/simple-repeater-table/SimpleRepeaterTableConfigPanel"),
|
||||
"modal-repeater-table": () => import("@/lib/registry/components/modal-repeater-table/ModalRepeaterTableConfigPanel"),
|
||||
"repeat-screen-modal": () => import("@/lib/registry/components/repeat-screen-modal/RepeatScreenModalConfigPanel"),
|
||||
"related-data-buttons": () => import("@/lib/registry/components/related-data-buttons/RelatedDataButtonsConfigPanel"),
|
||||
|
||||
// ========== 검색/선택 ==========
|
||||
"autocomplete-search-input": () => import("@/lib/registry/components/autocomplete-search-input/AutocompleteSearchInputConfigPanel"),
|
||||
"entity-search-input": () => import("@/lib/registry/components/entity-search-input/EntitySearchInputConfigPanel"),
|
||||
"selected-items-detail-input": () => import("@/lib/registry/components/selected-items-detail-input/SelectedItemsDetailInputConfigPanel"),
|
||||
"customer-item-mapping": () => import("@/lib/registry/components/customer-item-mapping/CustomerItemMappingConfigPanel"),
|
||||
"mail-recipient-selector": () => import("@/lib/registry/components/mail-recipient-selector/MailRecipientSelectorConfigPanel"),
|
||||
"location-swap-selector": () => import("@/lib/registry/components/location-swap-selector/LocationSwapSelectorConfigPanel"),
|
||||
|
||||
// ========== 특수 컴포넌트 ==========
|
||||
"flow-widget": () => import("@/components/screen/config-panels/FlowWidgetConfigPanel"),
|
||||
"tabs-widget": () => import("@/components/screen/config-panels/TabsConfigPanel"),
|
||||
"map": () => import("@/lib/registry/components/map/MapConfigPanel"),
|
||||
"rack-structure": () => import("@/lib/registry/components/rack-structure/RackStructureConfigPanel"),
|
||||
"aggregation-widget": () => import("@/lib/registry/components/aggregation-widget/AggregationWidgetConfigPanel"),
|
||||
"numbering-rule": () => import("@/lib/registry/components/numbering-rule/NumberingRuleConfigPanel"),
|
||||
"category-manager": () => import("@/lib/registry/components/category-manager/CategoryManagerConfigPanel"),
|
||||
"universal-form-modal": () => import("@/lib/registry/components/universal-form-modal/UniversalFormModalConfigPanel"),
|
||||
};
|
||||
|
||||
// ConfigPanel 컴포넌트 캐시
|
||||
|
|
@ -68,16 +96,43 @@ export async function getComponentConfigPanel(componentId: string): Promise<Reac
|
|||
const module = await importFn();
|
||||
|
||||
// 모듈에서 ConfigPanel 컴포넌트 추출
|
||||
// 1차: PascalCase 변환된 이름으로 찾기 (예: text-input -> TextInputConfigPanel)
|
||||
// 2차: 특수 export명들 fallback
|
||||
// 3차: default export
|
||||
const pascalCaseName = `${toPascalCase(componentId)}ConfigPanel`;
|
||||
const ConfigPanelComponent =
|
||||
module[`${toPascalCase(componentId)}ConfigPanel`] ||
|
||||
module.RepeaterConfigPanel || // repeater-field-group의 export명
|
||||
module.FlowWidgetConfigPanel || // flow-widget의 export명
|
||||
module.CustomerItemMappingConfigPanel || // customer-item-mapping의 export명
|
||||
module.SelectedItemsDetailInputConfigPanel || // selected-items-detail-input의 export명
|
||||
module.ButtonConfigPanel || // button-primary의 export명
|
||||
module.SectionCardConfigPanel || // section-card의 export명
|
||||
module.SectionPaperConfigPanel || // section-paper의 export명
|
||||
module.TabsConfigPanel || // tabs-widget의 export명
|
||||
module[pascalCaseName] ||
|
||||
// 특수 export명들
|
||||
module.RepeaterConfigPanel ||
|
||||
module.FlowWidgetConfigPanel ||
|
||||
module.CustomerItemMappingConfigPanel ||
|
||||
module.SelectedItemsDetailInputConfigPanel ||
|
||||
module.ButtonConfigPanel ||
|
||||
module.SectionCardConfigPanel ||
|
||||
module.SectionPaperConfigPanel ||
|
||||
module.TabsConfigPanel ||
|
||||
module.UnifiedRepeaterConfigPanel ||
|
||||
module.RepeatContainerConfigPanel ||
|
||||
module.ScreenSplitPanelConfigPanel ||
|
||||
module.SimpleRepeaterTableConfigPanel ||
|
||||
module.ModalRepeaterTableConfigPanel ||
|
||||
module.RepeatScreenModalConfigPanel ||
|
||||
module.RelatedDataButtonsConfigPanel ||
|
||||
module.AutocompleteSearchInputConfigPanel ||
|
||||
module.EntitySearchInputConfigPanel ||
|
||||
module.MailRecipientSelectorConfigPanel ||
|
||||
module.LocationSwapSelectorConfigPanel ||
|
||||
module.MapConfigPanel ||
|
||||
module.RackStructureConfigPanel ||
|
||||
module.AggregationWidgetConfigPanel ||
|
||||
module.NumberingRuleConfigPanel ||
|
||||
module.CategoryManagerConfigPanel ||
|
||||
module.UniversalFormModalConfigPanel ||
|
||||
module.PivotGridConfigPanel ||
|
||||
module.TableSearchWidgetConfigPanel ||
|
||||
module.TaxInvoiceListConfigPanel ||
|
||||
module.ImageWidgetConfigPanel ||
|
||||
module.TestInputConfigPanel ||
|
||||
module.default;
|
||||
|
||||
if (!ConfigPanelComponent) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue