From 0658ce41f9f6e606c3f2916b7774b40c3c5ed263 Mon Sep 17 00:00:00 2001 From: kjs Date: Mon, 19 Jan 2026 14:01:21 +0900 Subject: [PATCH] =?UTF-8?q?=EB=A6=AC=ED=94=BC=ED=84=B0=20=EC=BB=A8?= =?UTF-8?q?=ED=85=8C=EC=9D=B4=EB=84=88=20=EC=A0=9C=EB=AA=A9=20=EB=B0=8F=20?= =?UTF-8?q?=EC=84=A4=EB=AA=85=20=EC=84=A4=EC=A0=95=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80:=20RepeatContainerComponent=EC=99=80=20Repea?= =?UTF-8?q?tContainerConfigPanel=EC=97=90=EC=84=9C=20=EC=95=84=EC=9D=B4?= =?UTF-8?q?=ED=85=9C=20=EC=A0=9C=EB=AA=A9=EA=B3=BC=20=EC=84=A4=EB=AA=85?= =?UTF-8?q?=EC=9D=84=20=EC=84=A4=EC=A0=95=ED=95=A0=20=EC=88=98=20=EC=9E=88?= =?UTF-8?q?=EB=8A=94=20=EA=B8=B0=EB=8A=A5=EC=9D=84=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=ED=95=98=EC=98=80=EC=8A=B5=EB=8B=88=EB=8B=A4.=20=EC=A0=9C?= =?UTF-8?q?=EB=AA=A9=20=EB=B0=8F=20=EC=84=A4=EB=AA=85=20=EC=BB=AC=EB=9F=BC?= =?UTF-8?q?=EC=9D=84=20=EC=84=A0=ED=83=9D=ED=95=A0=20=EC=88=98=20=EC=9E=88?= =?UTF-8?q?=EB=8A=94=20=EC=BD=A4=EB=B3=B4=EB=B0=95=EC=8A=A4=EB=A5=BC=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=ED=95=98=EA=B3=A0,=20=EA=B0=81=20=EC=95=84?= =?UTF-8?q?=EC=9D=B4=ED=85=9C=EC=9D=98=20=EC=A0=9C=EB=AA=A9=EA=B3=BC=20?= =?UTF-8?q?=EC=84=A4=EB=AA=85=EC=9D=84=20=EB=8F=99=EC=A0=81=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=ED=91=9C=EC=8B=9C=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0=ED=95=98=EC=98=80=EC=8A=B5=EB=8B=88=EB=8B=A4?= =?UTF-8?q?.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../RepeatContainerComponent.tsx | 103 ++- .../RepeatContainerConfigPanel.tsx | 633 +++++++++--------- .../components/repeat-container/types.ts | 12 +- 3 files changed, 396 insertions(+), 352 deletions(-) diff --git a/frontend/lib/registry/components/repeat-container/RepeatContainerComponent.tsx b/frontend/lib/registry/components/repeat-container/RepeatContainerComponent.tsx index b78e6f0c..b51b2448 100644 --- a/frontend/lib/registry/components/repeat-container/RepeatContainerComponent.tsx +++ b/frontend/lib/registry/components/repeat-container/RepeatContainerComponent.tsx @@ -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, 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 => { + if (!showItemTitle || !descriptionColumn) return ""; + return String(rowData[descriptionColumn] ?? ""); + }, + [showItemTitle, descriptionColumn] ); // 아이템 클릭 핸들러 @@ -501,16 +524,29 @@ export function RepeatContainerComponent({ "ring-2 ring-blue-500" )} > - {showItemTitle && ( -
- {generateTitle(row, index)} + {showItemTitle && (titleColumn || itemTitleTemplate) && ( +
+
+ {generateTitle(row, index)} +
+ {descriptionColumn && generateDescription(row) && ( +
+ {generateDescription(row)} +
+ )}
)} @@ -608,16 +644,29 @@ export function RepeatContainerComponent({ )} onClick={() => handleItemClick(index, row)} > - {showItemTitle && ( -
- {generateTitle(row, index)} + {showItemTitle && (titleColumn || itemTitleTemplate) && ( +
+
+ {generateTitle(row, index)} +
+ {descriptionColumn && generateDescription(row) && ( +
+ {generateDescription(row)} +
+ )}
)} diff --git a/frontend/lib/registry/components/repeat-container/RepeatContainerConfigPanel.tsx b/frontend/lib/registry/components/repeat-container/RepeatContainerConfigPanel.tsx index 56ae79a6..2eddd234 100644 --- a/frontend/lib/registry/components/repeat-container/RepeatContainerConfigPanel.tsx +++ b/frontend/lib/registry/components/repeat-container/RepeatContainerConfigPanel.tsx @@ -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>([]); 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 (
@@ -416,7 +421,7 @@ export function RepeatContainerConfigPanel({
- {/* 아이템 제목 설정 */} + {/* 아이템 제목/설명 설정 */}
onChange({ showItemTitle: checked as boolean })} />

{config.showItemTitle && ( -
+
+ {/* 제목 컬럼 선택 (Combobox) */}
- - onChange({ itemTitleTemplate: e.target.value })} - placeholder="{order_no} - {item_code}" - className="h-8 text-xs" - /> -

- {"{필드명}"} 형식으로 데이터 바인딩 가능 -

+ + + + + + + + + + 컬럼을 찾을 수 없습니다 + + { + onChange({ titleColumn: "" }); + setTitleColumnOpen(false); + }} + className="text-xs" + > + + 선택 안함 + + {availableColumns.map((col) => ( + { + onChange({ titleColumn: col.columnName }); + setTitleColumnOpen(false); + }} + className="text-xs" + > + +
+ {col.displayName || col.columnName} + {col.displayName && col.displayName !== col.columnName && ( + {col.columnName} + )} +
+
+ ))} +
+
+
+
+
+ {config.titleColumn && ( +

+ 각 아이템의 "{config.titleColumn}" 값이 제목으로 표시됩니다 +

+ )}
-
-
- - onChange({ titleFontSize: e.target.value })} - placeholder="14px" - className="h-7 text-xs" - /> -
-
- - onChange({ titleColor: e.target.value })} - className="h-7" - /> -
-
- - + {/* 설명 컬럼 선택 (Combobox) */} +
+ + + + + + + + + + 컬럼을 찾을 수 없습니다 + + { + onChange({ descriptionColumn: "" }); + setDescriptionColumnOpen(false); + }} + className="text-xs" + > + + 선택 안함 + + {availableColumns.map((col) => ( + { + onChange({ descriptionColumn: col.columnName }); + setDescriptionColumnOpen(false); + }} + className="text-xs" + > + +
+ {col.displayName || col.columnName} + {col.displayName && col.displayName !== col.columnName && ( + {col.columnName} + )} +
+
+ ))} +
+
+
+
+
+ {config.descriptionColumn && ( +

+ 각 아이템의 "{config.descriptionColumn}" 값이 설명으로 표시됩니다 +

+ )} +
+ + {/* 제목 스타일 설정 */} +
+ +
+
+ + +
+
+ + onChange({ titleColor: e.target.value })} + className="h-7" + /> +
+
+ + +
+ + {/* 설명 스타일 설정 */} + {config.descriptionColumn && ( +
+ +
+
+ + +
+
+ + onChange({ descriptionColor: e.target.value })} + className="h-7" + /> +
+
+
+ )}
)}
@@ -600,82 +774,6 @@ export function RepeatContainerConfigPanel({ // 슬롯 자식 컴포넌트 관리 섹션 // ============================================================ -// 슬롯 컴포넌트의 전체 설정 패널을 표시하는 컴포넌트 -interface SlotComponentDetailPanelProps { - child: SlotComponentConfig; - screenTableName?: string; - availableColumns: Array<{ columnName: string; displayName?: string }>; - onConfigChange: (newConfig: Record) => void; - onFieldNameChange: (fieldName: string) => void; - onLabelChange: (label: string) => void; -} - -function SlotComponentDetailPanel({ - child, - screenTableName, - availableColumns, - onConfigChange, - onFieldNameChange, - onLabelChange, -}: SlotComponentDetailPanelProps) { - return ( -
- {/* 데이터 필드 바인딩 - 모든 컴포넌트에서 사용 가능 */} -
- - - {child.fieldName && ( -

- 각 아이템의 "{child.fieldName}" 값이 이 컴포넌트에 표시됩니다 -

- )} -
- - {/* 라벨 설정 */} -
- - onLabelChange(e.target.value)} - placeholder="표시할 라벨" - className="h-7 text-xs" - /> -
- - {/* 컴포넌트 전용 설정 */} -
-
- {child.componentType} 상세 설정 -
- -
-
- ); -} - interface SlotChildrenSectionProps { config: RepeatContainerConfig; onChange: (config: Partial) => void; @@ -691,14 +789,11 @@ function SlotChildrenSection({ loadingColumns, screenTableName, }: SlotChildrenSectionProps) { - const [selectedColumn, setSelectedColumn] = useState(""); const [columnComboboxOpen, setColumnComboboxOpen] = useState(false); - // 각 컴포넌트별 상세 설정 열림 상태 const [expandedIds, setExpandedIds] = useState>(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" > - {/* 기본 정보 헤더 */} + {/* 기본 정보 헤더 - 타입 선택 드롭다운 제거됨 */}
{index + 1} @@ -830,20 +904,6 @@ function SlotChildrenSection({ 필드: {child.fieldName}
-
- {/* 상세 설정 패널 (펼침) */} + {/* 상세 설정 패널 */} {isExpanded && (
- {/* 전용 ConfigPanel이 있는 복잡한 컴포넌트인 경우 */} {hasComponentConfigPanel(child.componentType) ? ( { onChange({ children: children.map((c) => @@ -885,41 +943,24 @@ function SlotChildrenSection({ ), }); }} - onFieldNameChange={(fieldName) => updateComponentFieldName(child.id, fieldName)} onLabelChange={(label) => updateComponentLabel(child.id, label)} /> ) : ( <> - {/* 데이터 필드 바인딩 - 가장 중요! */} -
- - - {child.fieldName && ( -

- 각 아이템의 "{child.fieldName}" 값이 표시됩니다 + {child.fieldName && ( +

+
+ + + 바인딩: {child.fieldName} + +
+

+ 각 아이템의 "{child.fieldName}" 값이 자동으로 표시됩니다

- )} -
+
+ )} - {/* 라벨 설정 */}
- {/* 크기 설정 */}
@@ -956,7 +996,6 @@ function SlotChildrenSection({
- {/* 스타일 설정 */}
@@ -970,124 +1009,25 @@ function SlotChildrenSection({ - 10px (아주 작게) - 12px (작게) - 14px (보통) - 16px (크게) - 18px (아주 크게) - - -
-
- - -
-
-
-
- -
-
- updateComponentStyle(child.id, "color", e.target.value)} - className="h-7 w-10 p-0.5 cursor-pointer" - /> - updateComponentStyle(child.id, "color", e.target.value)} - className="h-7 flex-1 text-xs" - placeholder="#000000" - /> -
+ updateComponentStyle(child.id, "color", e.target.value)} + className="h-7" + />
- - {/* 컴포넌트 타입별 추가 설정 */} - {(child.componentType === "number-display" || child.componentType === "number-input") && ( -
- -
-
- - - updateComponentConfig(child.id, "decimalPlaces", parseInt(e.target.value) || 0) - } - className="h-7 text-xs" - /> -
-
- - updateComponentConfig(child.id, "thousandSeparator", checked) - } - /> - -
-
-
- )} - - {(child.componentType === "date-display" || child.componentType === "date-input") && ( -
- - -
- )} )}
@@ -1173,14 +1113,61 @@ function SlotChildrenSection({
- -
-

- 안내: 여기는 필드 단위 컴포넌트만 추가하세요.
- 집계 위젯, 분할 패널 등은 리피터 외부에 별도로 배치해야 합니다. -

-
); } +// 슬롯 컴포넌트 상세 설정 패널 +interface SlotComponentDetailPanelProps { + child: SlotComponentConfig; + screenTableName?: string; + onConfigChange: (newConfig: Record) => void; + onLabelChange: (label: string) => void; +} + +function SlotComponentDetailPanel({ + child, + screenTableName, + onConfigChange, + onLabelChange, +}: SlotComponentDetailPanelProps) { + return ( +
+ {child.fieldName && ( +
+
+ + + 바인딩: {child.fieldName} + +
+

+ 각 아이템의 "{child.fieldName}" 값이 자동으로 표시됩니다 +

+
+ )} + +
+ + onLabelChange(e.target.value)} + placeholder="표시할 라벨" + className="h-7 text-xs" + /> +
+ +
+
+ {child.componentType} 상세 설정 +
+ +
+
+ ); +} diff --git a/frontend/lib/registry/components/repeat-container/types.ts b/frontend/lib/registry/components/repeat-container/types.ts index 642f5647..961c11e7 100644 --- a/frontend/lib/registry/components/repeat-container/types.ts +++ b/frontend/lib/registry/components/repeat-container/types.ts @@ -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. 데이터 필터링 (선택사항)