리피터 컨테이너 제목 및 설명 설정 기능 추가: RepeatContainerComponent와 RepeatContainerConfigPanel에서 아이템 제목과 설명을 설정할 수 있는 기능을 추가하였습니다. 제목 및 설명 컬럼을 선택할 수 있는 콤보박스를 구현하고, 각 아이템의 제목과 설명을 동적으로 표시하도록 개선하였습니다.
This commit is contained in:
parent
6ea3aef396
commit
0658ce41f9
|
|
@ -63,9 +63,13 @@ export function RepeatContainerComponent({
|
|||
padding: "16px",
|
||||
showItemTitle: false,
|
||||
itemTitleTemplate: "",
|
||||
titleColumn: "",
|
||||
descriptionColumn: "",
|
||||
titleFontSize: "14px",
|
||||
titleColor: "#374151",
|
||||
titleFontWeight: "600",
|
||||
descriptionFontSize: "12px",
|
||||
descriptionColor: "#6b7280",
|
||||
emptyMessage: "데이터가 없습니다",
|
||||
usePaging: false,
|
||||
pageSize: 10,
|
||||
|
|
@ -96,9 +100,13 @@ export function RepeatContainerComponent({
|
|||
padding,
|
||||
showItemTitle,
|
||||
itemTitleTemplate,
|
||||
titleColumn,
|
||||
descriptionColumn,
|
||||
titleFontSize,
|
||||
titleColor,
|
||||
titleFontWeight,
|
||||
descriptionFontSize,
|
||||
descriptionColor,
|
||||
filterField,
|
||||
filterColumn,
|
||||
useGrouping,
|
||||
|
|
@ -229,20 +237,35 @@ export function RepeatContainerComponent({
|
|||
return Math.ceil(filteredData.length / pageSize);
|
||||
}, [filteredData.length, usePaging, pageSize]);
|
||||
|
||||
// 아이템 제목 생성
|
||||
// 아이템 제목 생성 (titleColumn 우선, 없으면 itemTitleTemplate 사용)
|
||||
const generateTitle = useCallback(
|
||||
(rowData: Record<string, any>, index: number): string => {
|
||||
if (!showItemTitle) return "";
|
||||
|
||||
if (!itemTitleTemplate) {
|
||||
return `아이템 ${index + 1}`;
|
||||
// titleColumn이 설정된 경우 해당 컬럼 값 사용
|
||||
if (titleColumn) {
|
||||
return String(rowData[titleColumn] ?? "");
|
||||
}
|
||||
|
||||
return itemTitleTemplate.replace(/\{([^}]+)\}/g, (match, field) => {
|
||||
return String(rowData[field] ?? "");
|
||||
});
|
||||
// 레거시: itemTitleTemplate 사용
|
||||
if (itemTitleTemplate) {
|
||||
return itemTitleTemplate.replace(/\{([^}]+)\}/g, (match, field) => {
|
||||
return String(rowData[field] ?? "");
|
||||
});
|
||||
}
|
||||
|
||||
return `아이템 ${index + 1}`;
|
||||
},
|
||||
[showItemTitle, itemTitleTemplate]
|
||||
[showItemTitle, titleColumn, itemTitleTemplate]
|
||||
);
|
||||
|
||||
// 아이템 설명 생성
|
||||
const generateDescription = useCallback(
|
||||
(rowData: Record<string, any>): string => {
|
||||
if (!showItemTitle || !descriptionColumn) return "";
|
||||
return String(rowData[descriptionColumn] ?? "");
|
||||
},
|
||||
[showItemTitle, descriptionColumn]
|
||||
);
|
||||
|
||||
// 아이템 클릭 핸들러
|
||||
|
|
@ -501,16 +524,29 @@ export function RepeatContainerComponent({
|
|||
"ring-2 ring-blue-500"
|
||||
)}
|
||||
>
|
||||
{showItemTitle && (
|
||||
<div
|
||||
className="mb-2 border-b pb-2 font-medium"
|
||||
style={{
|
||||
fontSize: titleFontSize,
|
||||
color: titleColor,
|
||||
fontWeight: titleFontWeight,
|
||||
}}
|
||||
>
|
||||
{generateTitle(row, index)}
|
||||
{showItemTitle && (titleColumn || itemTitleTemplate) && (
|
||||
<div className="mb-2 border-b pb-2">
|
||||
<div
|
||||
className="font-medium"
|
||||
style={{
|
||||
fontSize: titleFontSize,
|
||||
color: titleColor,
|
||||
fontWeight: titleFontWeight,
|
||||
}}
|
||||
>
|
||||
{generateTitle(row, index)}
|
||||
</div>
|
||||
{descriptionColumn && generateDescription(row) && (
|
||||
<div
|
||||
className="mt-1"
|
||||
style={{
|
||||
fontSize: descriptionFontSize,
|
||||
color: descriptionColor,
|
||||
}}
|
||||
>
|
||||
{generateDescription(row)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
@ -608,16 +644,29 @@ export function RepeatContainerComponent({
|
|||
)}
|
||||
onClick={() => handleItemClick(index, row)}
|
||||
>
|
||||
{showItemTitle && (
|
||||
<div
|
||||
className="mb-2 border-b pb-2"
|
||||
style={{
|
||||
fontSize: titleFontSize,
|
||||
color: titleColor,
|
||||
fontWeight: titleFontWeight,
|
||||
}}
|
||||
>
|
||||
{generateTitle(row, index)}
|
||||
{showItemTitle && (titleColumn || itemTitleTemplate) && (
|
||||
<div className="mb-2 border-b pb-2">
|
||||
<div
|
||||
className="font-medium"
|
||||
style={{
|
||||
fontSize: titleFontSize,
|
||||
color: titleColor,
|
||||
fontWeight: titleFontWeight,
|
||||
}}
|
||||
>
|
||||
{generateTitle(row, index)}
|
||||
</div>
|
||||
{descriptionColumn && generateDescription(row) && (
|
||||
<div
|
||||
className="mt-1"
|
||||
style={{
|
||||
fontSize: descriptionFontSize,
|
||||
color: descriptionColor,
|
||||
}}
|
||||
>
|
||||
{generateDescription(row)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ 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, Settings2, ChevronDown, ChevronUp } from "lucide-react";
|
||||
import { Database, Table2, ChevronsUpDown, Check, LayoutGrid, LayoutList, Rows3, Plus, X, Type, Settings2, ChevronUp } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { RepeatContainerConfig, SlotComponentConfig } from "./types";
|
||||
import { tableTypeApi } from "@/lib/api/screen";
|
||||
|
|
@ -42,6 +42,10 @@ export function RepeatContainerConfigPanel({
|
|||
// 컬럼 관련 상태
|
||||
const [availableColumns, setAvailableColumns] = useState<Array<{ columnName: string; displayName?: string }>>([]);
|
||||
const [loadingColumns, setLoadingColumns] = useState(false);
|
||||
|
||||
// 제목/설명 컬럼 콤보박스 상태
|
||||
const [titleColumnOpen, setTitleColumnOpen] = useState(false);
|
||||
const [descriptionColumnOpen, setDescriptionColumnOpen] = useState(false);
|
||||
|
||||
// 실제 사용할 테이블 이름 계산
|
||||
const targetTableName = useMemo(() => {
|
||||
|
|
@ -90,13 +94,14 @@ export function RepeatContainerConfigPanel({
|
|||
setLoadingColumns(true);
|
||||
try {
|
||||
const response = await tableManagementApi.getColumnList(targetTableName);
|
||||
if (response.success && response.data && Array.isArray(response.data)) {
|
||||
setAvailableColumns(
|
||||
response.data.map((col: any) => ({
|
||||
columnName: col.columnName,
|
||||
displayName: col.displayName || col.columnLabel || col.columnName,
|
||||
}))
|
||||
);
|
||||
// API 응답이 { data: { columns: [...] } } 또는 { data: [...] } 형태일 수 있음
|
||||
const columnsData = response.data?.columns || response.data;
|
||||
if (response.success && columnsData && Array.isArray(columnsData)) {
|
||||
const columns = columnsData.map((col: any) => ({
|
||||
columnName: col.columnName,
|
||||
displayName: col.displayName || col.columnLabel || col.columnName,
|
||||
}));
|
||||
setAvailableColumns(columns);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("컬럼 목록 가져오기 실패:", error);
|
||||
|
|
@ -106,7 +111,7 @@ export function RepeatContainerConfigPanel({
|
|||
}
|
||||
};
|
||||
fetchColumns();
|
||||
}, [targetTableName]);
|
||||
}, [targetTableName, config.tableName, screenTableName, config.useCustomTable, config.customTableName]);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
|
|
@ -416,7 +421,7 @@ export function RepeatContainerConfigPanel({
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* 아이템 제목 설정 */}
|
||||
{/* 아이템 제목/설명 설정 */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
|
|
@ -425,63 +430,232 @@ export function RepeatContainerConfigPanel({
|
|||
onCheckedChange={(checked) => onChange({ showItemTitle: checked as boolean })}
|
||||
/>
|
||||
<Label htmlFor="showItemTitle" className="text-sm font-semibold">
|
||||
아이템 제목 표시
|
||||
아이템 제목/설명 표시
|
||||
</Label>
|
||||
</div>
|
||||
<hr className="border-border" />
|
||||
|
||||
{config.showItemTitle && (
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-3">
|
||||
{/* 제목 컬럼 선택 (Combobox) */}
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">제목 템플릿</Label>
|
||||
<Input
|
||||
value={config.itemTitleTemplate || ""}
|
||||
onChange={(e) => onChange({ itemTitleTemplate: e.target.value })}
|
||||
placeholder="{order_no} - {item_code}"
|
||||
className="h-8 text-xs"
|
||||
/>
|
||||
<p className="text-[10px] text-muted-foreground">
|
||||
{"{필드명}"} 형식으로 데이터 바인딩 가능
|
||||
</p>
|
||||
<Label className="text-xs font-medium">제목 컬럼</Label>
|
||||
<Popover open={titleColumnOpen} onOpenChange={setTitleColumnOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={titleColumnOpen}
|
||||
className="h-8 w-full justify-between text-xs font-normal"
|
||||
disabled={loadingColumns || availableColumns.length === 0}
|
||||
>
|
||||
{loadingColumns
|
||||
? "로딩 중..."
|
||||
: config.titleColumn
|
||||
? availableColumns.find(c => c.columnName === config.titleColumn)?.displayName || config.titleColumn
|
||||
: "제목 컬럼 선택..."}
|
||||
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[280px] p-0" align="start">
|
||||
<Command>
|
||||
<CommandInput placeholder="컬럼 검색..." className="h-8 text-xs" />
|
||||
<CommandList>
|
||||
<CommandEmpty className="py-2 text-xs text-center">컬럼을 찾을 수 없습니다</CommandEmpty>
|
||||
<CommandGroup>
|
||||
<CommandItem
|
||||
value="__none__"
|
||||
onSelect={() => {
|
||||
onChange({ titleColumn: "" });
|
||||
setTitleColumnOpen(false);
|
||||
}}
|
||||
className="text-xs"
|
||||
>
|
||||
<Check className={cn("mr-2 h-3 w-3", !config.titleColumn ? "opacity-100" : "opacity-0")} />
|
||||
선택 안함
|
||||
</CommandItem>
|
||||
{availableColumns.map((col) => (
|
||||
<CommandItem
|
||||
key={col.columnName}
|
||||
value={col.columnName}
|
||||
onSelect={() => {
|
||||
onChange({ titleColumn: col.columnName });
|
||||
setTitleColumnOpen(false);
|
||||
}}
|
||||
className="text-xs"
|
||||
>
|
||||
<Check className={cn("mr-2 h-3 w-3", config.titleColumn === col.columnName ? "opacity-100" : "opacity-0")} />
|
||||
<div className="flex flex-col">
|
||||
<span>{col.displayName || col.columnName}</span>
|
||||
{col.displayName && col.displayName !== col.columnName && (
|
||||
<span className="text-[10px] text-muted-foreground">{col.columnName}</span>
|
||||
)}
|
||||
</div>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
{config.titleColumn && (
|
||||
<p className="text-[10px] text-green-600">
|
||||
각 아이템의 "{config.titleColumn}" 값이 제목으로 표시됩니다
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[10px]">제목 크기</Label>
|
||||
<Input
|
||||
value={config.titleFontSize || "14px"}
|
||||
onChange={(e) => onChange({ titleFontSize: e.target.value })}
|
||||
placeholder="14px"
|
||||
className="h-7 text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[10px]">제목 색상</Label>
|
||||
<Input
|
||||
type="color"
|
||||
value={config.titleColor || "#374151"}
|
||||
onChange={(e) => onChange({ titleColor: e.target.value })}
|
||||
className="h-7"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[10px]">굵기</Label>
|
||||
<Select
|
||||
value={config.titleFontWeight || "600"}
|
||||
onValueChange={(value) => onChange({ titleFontWeight: value })}
|
||||
>
|
||||
<SelectTrigger className="h-7 text-xs">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="400">보통</SelectItem>
|
||||
<SelectItem value="500">중간</SelectItem>
|
||||
<SelectItem value="600">굵게</SelectItem>
|
||||
<SelectItem value="700">아주 굵게</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{/* 설명 컬럼 선택 (Combobox) */}
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs font-medium">설명 컬럼 (선택사항)</Label>
|
||||
<Popover open={descriptionColumnOpen} onOpenChange={setDescriptionColumnOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={descriptionColumnOpen}
|
||||
className="h-8 w-full justify-between text-xs font-normal"
|
||||
disabled={loadingColumns || availableColumns.length === 0}
|
||||
>
|
||||
{loadingColumns
|
||||
? "로딩 중..."
|
||||
: config.descriptionColumn
|
||||
? availableColumns.find(c => c.columnName === config.descriptionColumn)?.displayName || config.descriptionColumn
|
||||
: "설명 컬럼 선택..."}
|
||||
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[280px] p-0" align="start">
|
||||
<Command>
|
||||
<CommandInput placeholder="컬럼 검색..." className="h-8 text-xs" />
|
||||
<CommandList>
|
||||
<CommandEmpty className="py-2 text-xs text-center">컬럼을 찾을 수 없습니다</CommandEmpty>
|
||||
<CommandGroup>
|
||||
<CommandItem
|
||||
value="__none__"
|
||||
onSelect={() => {
|
||||
onChange({ descriptionColumn: "" });
|
||||
setDescriptionColumnOpen(false);
|
||||
}}
|
||||
className="text-xs"
|
||||
>
|
||||
<Check className={cn("mr-2 h-3 w-3", !config.descriptionColumn ? "opacity-100" : "opacity-0")} />
|
||||
선택 안함
|
||||
</CommandItem>
|
||||
{availableColumns.map((col) => (
|
||||
<CommandItem
|
||||
key={col.columnName}
|
||||
value={col.columnName}
|
||||
onSelect={() => {
|
||||
onChange({ descriptionColumn: col.columnName });
|
||||
setDescriptionColumnOpen(false);
|
||||
}}
|
||||
className="text-xs"
|
||||
>
|
||||
<Check className={cn("mr-2 h-3 w-3", config.descriptionColumn === col.columnName ? "opacity-100" : "opacity-0")} />
|
||||
<div className="flex flex-col">
|
||||
<span>{col.displayName || col.columnName}</span>
|
||||
{col.displayName && col.displayName !== col.columnName && (
|
||||
<span className="text-[10px] text-muted-foreground">{col.columnName}</span>
|
||||
)}
|
||||
</div>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
{config.descriptionColumn && (
|
||||
<p className="text-[10px] text-green-600">
|
||||
각 아이템의 "{config.descriptionColumn}" 값이 설명으로 표시됩니다
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 제목 스타일 설정 */}
|
||||
<div className="space-y-2 pt-2 border-t">
|
||||
<Label className="text-[10px] text-slate-500">제목 스타일</Label>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[10px]">크기</Label>
|
||||
<Select
|
||||
value={config.titleFontSize || "14px"}
|
||||
onValueChange={(value) => onChange({ titleFontSize: value })}
|
||||
>
|
||||
<SelectTrigger className="h-7 text-xs">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<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]">색상</Label>
|
||||
<Input
|
||||
type="color"
|
||||
value={config.titleColor || "#374151"}
|
||||
onChange={(e) => onChange({ titleColor: e.target.value })}
|
||||
className="h-7"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[10px]">굵기</Label>
|
||||
<Select
|
||||
value={config.titleFontWeight || "600"}
|
||||
onValueChange={(value) => onChange({ titleFontWeight: value })}
|
||||
>
|
||||
<SelectTrigger className="h-7 text-xs">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="400">보통</SelectItem>
|
||||
<SelectItem value="500">중간</SelectItem>
|
||||
<SelectItem value="600">굵게</SelectItem>
|
||||
<SelectItem value="700">아주 굵게</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 설명 스타일 설정 */}
|
||||
{config.descriptionColumn && (
|
||||
<div className="space-y-2">
|
||||
<Label className="text-[10px] text-slate-500">설명 스타일</Label>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[10px]">크기</Label>
|
||||
<Select
|
||||
value={config.descriptionFontSize || "12px"}
|
||||
onValueChange={(value) => onChange({ descriptionFontSize: 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>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[10px]">색상</Label>
|
||||
<Input
|
||||
type="color"
|
||||
value={config.descriptionColor || "#6b7280"}
|
||||
onChange={(e) => onChange({ descriptionColor: e.target.value })}
|
||||
className="h-7"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -600,82 +774,6 @@ 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;
|
||||
|
|
@ -691,14 +789,11 @@ function SlotChildrenSection({
|
|||
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);
|
||||
|
|
@ -711,7 +806,6 @@ function SlotChildrenSection({
|
|||
});
|
||||
};
|
||||
|
||||
// 컴포넌트 추가
|
||||
const addComponent = (columnName: string, displayName: string) => {
|
||||
const newChild: SlotComponentConfig = {
|
||||
id: `slot_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
||||
|
|
@ -727,11 +821,9 @@ function SlotChildrenSection({
|
|||
onChange({
|
||||
children: [...children, newChild],
|
||||
});
|
||||
setSelectedColumn("");
|
||||
setColumnComboboxOpen(false);
|
||||
};
|
||||
|
||||
// 컴포넌트 삭제
|
||||
const removeComponent = (id: string) => {
|
||||
onChange({
|
||||
children: children.filter((c) => c.id !== id),
|
||||
|
|
@ -743,28 +835,12 @@ function SlotChildrenSection({
|
|||
});
|
||||
};
|
||||
|
||||
// 컴포넌트 라벨 변경
|
||||
const updateComponentLabel = (id: string, label: string) => {
|
||||
onChange({
|
||||
children: children.map((c) => (c.id === id ? { ...c, label } : c)),
|
||||
});
|
||||
};
|
||||
|
||||
// 컴포넌트 타입 변경
|
||||
const updateComponentType = (id: string, componentType: string) => {
|
||||
onChange({
|
||||
children: children.map((c) => (c.id === id ? { ...c, componentType } : c)),
|
||||
});
|
||||
};
|
||||
|
||||
// 컴포넌트 필드 바인딩 변경
|
||||
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) =>
|
||||
|
|
@ -775,7 +851,6 @@ function SlotChildrenSection({
|
|||
});
|
||||
};
|
||||
|
||||
// 컴포넌트 스타일 변경
|
||||
const updateComponentStyle = (id: string, key: string, value: any) => {
|
||||
onChange({
|
||||
children: children.map((c) =>
|
||||
|
|
@ -786,7 +861,6 @@ function SlotChildrenSection({
|
|||
});
|
||||
};
|
||||
|
||||
// 컴포넌트 크기 변경
|
||||
const updateComponentSize = (id: string, width: number | undefined, height: number | undefined) => {
|
||||
onChange({
|
||||
children: children.map((c) =>
|
||||
|
|
@ -817,7 +891,7 @@ function SlotChildrenSection({
|
|||
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}
|
||||
|
|
@ -830,20 +904,6 @@ function SlotChildrenSection({
|
|||
필드: {child.fieldName}
|
||||
</div>
|
||||
</div>
|
||||
<Select
|
||||
value={child.componentType}
|
||||
onValueChange={(value) => updateComponentType(child.id, value)}
|
||||
>
|
||||
<SelectTrigger className="h-6 w-20 text-[10px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="text-display">텍스트</SelectItem>
|
||||
<SelectItem value="text-input">입력</SelectItem>
|
||||
<SelectItem value="number-display">숫자</SelectItem>
|
||||
<SelectItem value="date-display">날짜</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
|
|
@ -867,15 +927,13 @@ function SlotChildrenSection({
|
|||
</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) =>
|
||||
|
|
@ -885,41 +943,24 @@ function SlotChildrenSection({
|
|||
),
|
||||
});
|
||||
}}
|
||||
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}" 값이 표시됩니다
|
||||
{child.fieldName && (
|
||||
<div className="p-2 bg-green-50 rounded-md border border-green-200">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Database className="h-3 w-3 text-green-600" />
|
||||
<span className="text-[10px] text-green-700 font-medium">
|
||||
바인딩: {child.fieldName}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-[9px] text-green-600 mt-0.5">
|
||||
각 아이템의 "{child.fieldName}" 값이 자동으로 표시됩니다
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 라벨 설정 */}
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[10px] text-slate-500">표시 라벨</Label>
|
||||
<Input
|
||||
|
|
@ -930,7 +971,6 @@ function SlotChildrenSection({
|
|||
/>
|
||||
</div>
|
||||
|
||||
{/* 크기 설정 */}
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[10px] text-slate-500">너비 (px)</Label>
|
||||
|
|
@ -956,7 +996,6 @@ function SlotChildrenSection({
|
|||
</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">
|
||||
|
|
@ -970,124 +1009,25 @@ function SlotChildrenSection({
|
|||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<Input
|
||||
type="color"
|
||||
value={child.style?.color || "#000000"}
|
||||
onChange={(e) => updateComponentStyle(child.id, "color", e.target.value)}
|
||||
className="h-7"
|
||||
/>
|
||||
</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>
|
||||
|
|
@ -1173,14 +1113,61 @@ function SlotChildrenSection({
|
|||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
<div className="rounded-md border border-amber-200 bg-amber-50 p-2">
|
||||
<p className="text-[10px] text-amber-700">
|
||||
<strong>안내:</strong> 여기는 필드 단위 컴포넌트만 추가하세요.<br />
|
||||
집계 위젯, 분할 패널 등은 리피터 외부에 별도로 배치해야 합니다.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 슬롯 컴포넌트 상세 설정 패널
|
||||
interface SlotComponentDetailPanelProps {
|
||||
child: SlotComponentConfig;
|
||||
screenTableName?: string;
|
||||
onConfigChange: (newConfig: Record<string, any>) => void;
|
||||
onLabelChange: (label: string) => void;
|
||||
}
|
||||
|
||||
function SlotComponentDetailPanel({
|
||||
child,
|
||||
screenTableName,
|
||||
onConfigChange,
|
||||
onLabelChange,
|
||||
}: SlotComponentDetailPanelProps) {
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{child.fieldName && (
|
||||
<div className="p-2 bg-green-50 rounded-md border border-green-200">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Database className="h-3 w-3 text-green-600" />
|
||||
<span className="text-[10px] text-green-700 font-medium">
|
||||
바인딩: {child.fieldName}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-[9px] text-green-600 mt-0.5">
|
||||
각 아이템의 "{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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -86,18 +86,26 @@ export interface RepeatContainerConfig extends ComponentConfig {
|
|||
padding?: string;
|
||||
|
||||
// ========================
|
||||
// 4. 제목 설정 (각 아이템)
|
||||
// 4. 제목/설명 설정 (각 아이템)
|
||||
// ========================
|
||||
/** 아이템 제목 표시 */
|
||||
showItemTitle?: boolean;
|
||||
/** 아이템 제목 템플릿 (예: "{order_no} - {item_code}") */
|
||||
/** 아이템 제목 템플릿 (예: "{order_no} - {item_code}") - 레거시 */
|
||||
itemTitleTemplate?: string;
|
||||
/** 제목으로 사용할 컬럼명 */
|
||||
titleColumn?: string;
|
||||
/** 설명으로 사용할 컬럼명 */
|
||||
descriptionColumn?: string;
|
||||
/** 제목 폰트 크기 */
|
||||
titleFontSize?: string;
|
||||
/** 제목 색상 */
|
||||
titleColor?: string;
|
||||
/** 제목 폰트 굵기 */
|
||||
titleFontWeight?: string;
|
||||
/** 설명 폰트 크기 */
|
||||
descriptionFontSize?: string;
|
||||
/** 설명 색상 */
|
||||
descriptionColor?: string;
|
||||
|
||||
// ========================
|
||||
// 5. 데이터 필터링 (선택사항)
|
||||
|
|
|
|||
Loading…
Reference in New Issue