From 994d9b70cd7b11a8c91e17431b3c01f4f01ff0dc Mon Sep 17 00:00:00 2001 From: dohyeons Date: Wed, 22 Oct 2025 15:29:57 +0900 Subject: [PATCH] =?UTF-8?q?=EC=82=AC=EC=9D=B4=EB=93=9C=EB=B0=94=20?= =?UTF-8?q?=EB=B0=A9=EC=8B=9D=20=EB=B3=80=EA=B2=BD=ED=95=98=EB=A9=B4?= =?UTF-8?q?=EC=84=9C=20=EC=83=9D=EA=B8=B4=20=EC=98=A4=EB=A5=98=20=ED=95=B4?= =?UTF-8?q?=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/dashboard/ElementConfigSidebar.tsx | 12 +- frontend/components/admin/dashboard/types.ts | 2 +- .../widgets/ListWidgetConfigSidebar.tsx | 109 +++------ .../widgets/YardWidgetConfigSidebar.tsx | 119 ++++++++++ .../widgets/list-widget/ColumnSelector.tsx | 175 ++++++++------ .../widgets/list-widget/ListTableOptions.tsx | 214 ++++++++--------- .../list-widget/ManualColumnEditor.tsx | 196 ++++++++-------- .../list-widget/UnifiedColumnEditor.tsx | 219 ++++++++++++++++++ 8 files changed, 679 insertions(+), 367 deletions(-) create mode 100644 frontend/components/admin/dashboard/widgets/YardWidgetConfigSidebar.tsx create mode 100644 frontend/components/admin/dashboard/widgets/list-widget/UnifiedColumnEditor.tsx diff --git a/frontend/components/admin/dashboard/ElementConfigSidebar.tsx b/frontend/components/admin/dashboard/ElementConfigSidebar.tsx index d770763a..97332944 100644 --- a/frontend/components/admin/dashboard/ElementConfigSidebar.tsx +++ b/frontend/components/admin/dashboard/ElementConfigSidebar.tsx @@ -8,7 +8,7 @@ import { VehicleMapConfigPanel } from "./VehicleMapConfigPanel"; import { DatabaseConfig } from "./data-sources/DatabaseConfig"; import { ApiConfig } from "./data-sources/ApiConfig"; import { ListWidgetConfigSidebar } from "./widgets/ListWidgetConfigSidebar"; -import { YardWidgetConfigModal } from "./widgets/YardWidgetConfigModal"; +import { YardWidgetConfigSidebar } from "./widgets/YardWidgetConfigSidebar"; import { X } from "lucide-react"; import { cn } from "@/lib/utils"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; @@ -131,16 +131,16 @@ export function ElementConfigSidebar({ element, isOpen, onClose, onApply }: Elem ); } - // 야드 위젯은 별도 모달로 처리 + // 야드 위젯은 사이드바로 처리 if (element.subtype === "yard-management-3d") { return ( - { - onApply({ ...element, ...updatedElement }); + onApply={(updates) => { + onApply({ ...element, ...updates }); }} + onClose={onClose} /> ); } diff --git a/frontend/components/admin/dashboard/types.ts b/frontend/components/admin/dashboard/types.ts index 6ce41b6f..7ae9b4d8 100644 --- a/frontend/components/admin/dashboard/types.ts +++ b/frontend/components/admin/dashboard/types.ts @@ -256,7 +256,7 @@ export interface ChartDataset { // 리스트 위젯 설정 export interface ListWidgetConfig { - columnMode: "auto" | "manual"; // 컬럼 설정 방식 (자동 or 수동) + columnMode?: "auto" | "manual"; // [Deprecated] 더 이상 사용하지 않음 (하위 호환성을 위해 유지) viewMode: "table" | "card"; // 뷰 모드 (테이블 or 카드) (기본: table) columns: ListColumn[]; // 컬럼 정의 pageSize: number; // 페이지당 행 수 (기본: 10) diff --git a/frontend/components/admin/dashboard/widgets/ListWidgetConfigSidebar.tsx b/frontend/components/admin/dashboard/widgets/ListWidgetConfigSidebar.tsx index 9ac31a45..62db5cef 100644 --- a/frontend/components/admin/dashboard/widgets/ListWidgetConfigSidebar.tsx +++ b/frontend/components/admin/dashboard/widgets/ListWidgetConfigSidebar.tsx @@ -8,8 +8,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { DatabaseConfig } from "../data-sources/DatabaseConfig"; import { ApiConfig } from "../data-sources/ApiConfig"; import { QueryEditor } from "../QueryEditor"; -import { ColumnSelector } from "./list-widget/ColumnSelector"; -import { ManualColumnEditor } from "./list-widget/ManualColumnEditor"; +import { UnifiedColumnEditor } from "./list-widget/UnifiedColumnEditor"; import { ListTableOptions } from "./list-widget/ListTableOptions"; interface ListWidgetConfigSidebarProps { @@ -30,7 +29,6 @@ export function ListWidgetConfigSidebar({ element, isOpen, onClose, onApply }: L const [queryResult, setQueryResult] = useState(null); const [listConfig, setListConfig] = useState( element.listConfig || { - columnMode: "auto", viewMode: "table", columns: [], pageSize: 10, @@ -96,22 +94,23 @@ export function ListWidgetConfigSidebar({ element, isOpen, onClose, onApply }: L const handleQueryTest = useCallback((result: QueryResult) => { setQueryResult(result); - // 자동 모드인 경우: 쿼리 결과의 컬럼을 자동으로 listConfig.columns에 반영 + // 쿼리 결과의 컬럼을 자동으로 listConfig.columns에 추가 (기존 컬럼은 유지) setListConfig((prev) => { - if (prev.columnMode === "auto") { - return { - ...prev, - columns: result.columns.map((col, idx) => ({ - id: `col_${idx}`, - key: col, - label: col, - visible: true, - width: "auto", - align: "left", - })), - }; - } - return prev; + const existingFields = prev.columns.map((col) => col.field); + const newColumns = result.columns + .filter((col) => !existingFields.includes(col)) + .map((col, idx) => ({ + id: `col_${Date.now()}_${idx}`, + field: col, + label: col, + visible: true, + align: "left" as const, + })); + + return { + ...prev, + columns: [...prev.columns, ...newColumns], + }; }); }, []); @@ -133,7 +132,7 @@ export function ListWidgetConfigSidebar({ element, isOpen, onClose, onApply }: L }, [element, title, dataSource, listConfig, onApply]); // 저장 가능 여부 - const canApply = queryResult && queryResult.rows.length > 0 && listConfig.columns.length > 0; + const canApply = listConfig.columns.length > 0 && listConfig.columns.some((col) => col.visible && col.field); return (
- - {/* 컬럼 설정 */} - {queryResult && queryResult.rows.length > 0 && ( -
-
컬럼 설정
- {listConfig.columnMode === "auto" ? ( - - ) : ( - - )} -
- )} - - {/* 테이블 옵션 */} - {queryResult && queryResult.rows.length > 0 && ( -
-
- 테이블 옵션 -
- -
- )} - - {/* 컬럼 설정 */} - {queryResult && queryResult.rows.length > 0 && ( -
-
컬럼 설정
- {listConfig.columnMode === "auto" ? ( - - ) : ( - - )} -
- )} - - {/* 테이블 옵션 */} - {queryResult && queryResult.rows.length > 0 && ( -
-
- 테이블 옵션 -
- -
- )}
@@ -275,6 +222,26 @@ export function ListWidgetConfigSidebar({ element, isOpen, onClose, onApply }: L
)} + + {/* 컬럼 설정 - 쿼리 실행 후에만 표시 */} + {queryResult && ( +
+
컬럼 설정
+ +
+ )} + + {/* 테이블 옵션 - 컬럼이 있을 때만 표시 */} + {listConfig.columns.length > 0 && ( +
+
테이블 옵션
+ +
+ )} {/* 푸터: 적용 버튼 */} diff --git a/frontend/components/admin/dashboard/widgets/YardWidgetConfigSidebar.tsx b/frontend/components/admin/dashboard/widgets/YardWidgetConfigSidebar.tsx new file mode 100644 index 00000000..12db1d21 --- /dev/null +++ b/frontend/components/admin/dashboard/widgets/YardWidgetConfigSidebar.tsx @@ -0,0 +1,119 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; +import { DashboardElement } from "../types"; +import { X } from "lucide-react"; +import { cn } from "@/lib/utils"; + +interface YardWidgetConfigSidebarProps { + element: DashboardElement; + isOpen: boolean; + onClose: () => void; + onApply: (updates: Partial) => void; +} + +export function YardWidgetConfigSidebar({ element, isOpen, onClose, onApply }: YardWidgetConfigSidebarProps) { + const [customTitle, setCustomTitle] = useState(element.customTitle || ""); + const [showHeader, setShowHeader] = useState(element.showHeader !== false); + + useEffect(() => { + if (isOpen) { + setCustomTitle(element.customTitle || ""); + setShowHeader(element.showHeader !== false); + } + }, [isOpen, element]); + + const handleApply = () => { + onApply({ + customTitle, + showHeader, + }); + onClose(); + }; + + return ( +
+ {/* 헤더 */} +
+
+
+ 🏗️ +
+ 야드 관리 위젯 설정 +
+ +
+ + {/* 컨텐츠 */} +
+
+ {/* 위젯 제목 */} +
+
위젯 제목
+ setCustomTitle(e.target.value)} + placeholder="제목을 입력하세요 (비워두면 기본 제목 사용)" + className="h-8 text-xs" + style={{ fontSize: "12px" }} + /> +

기본 제목: 야드 관리 3D

+
+ + {/* 헤더 표시 */} +
+
헤더 표시
+ setShowHeader(value === "show")} + className="flex items-center gap-3" + > +
+ + +
+
+ + +
+
+
+
+
+ + {/* 푸터 */} +
+ + +
+
+ ); +} diff --git a/frontend/components/admin/dashboard/widgets/list-widget/ColumnSelector.tsx b/frontend/components/admin/dashboard/widgets/list-widget/ColumnSelector.tsx index 37b0f157..46ac8332 100644 --- a/frontend/components/admin/dashboard/widgets/list-widget/ColumnSelector.tsx +++ b/frontend/components/admin/dashboard/widgets/list-widget/ColumnSelector.tsx @@ -1,19 +1,16 @@ "use client"; import React, { useState } from "react"; -import { ListColumn } from "../../types"; -import { Card } from "@/components/ui/card"; -import { Label } from "@/components/ui/label"; +import { ListColumn, QueryResult, ListWidgetConfig } from "../../types"; import { Input } from "@/components/ui/input"; import { Checkbox } from "@/components/ui/checkbox"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { GripVertical } from "lucide-react"; interface ColumnSelectorProps { - availableColumns: string[]; - selectedColumns: ListColumn[]; - sampleData: Record; - onChange: (columns: ListColumn[]) => void; + queryResult: QueryResult; + config: ListWidgetConfig; + onConfigChange: (updates: Partial) => void; } /** @@ -23,15 +20,18 @@ interface ColumnSelectorProps { * - 정렬, 너비, 정렬 방향 설정 * - 드래그 앤 드롭으로 순서 변경 */ -export function ColumnSelector({ availableColumns, selectedColumns, sampleData, onChange }: ColumnSelectorProps) { +export function ColumnSelector({ queryResult, config, onConfigChange }: ColumnSelectorProps) { const [draggedIndex, setDraggedIndex] = useState(null); - const [dragOverIndex, setDragOverIndex] = useState(null); + + const availableColumns = queryResult.columns; + const selectedColumns = config.columns || []; + const sampleData = queryResult.rows[0] || {}; // 컬럼 선택/해제 const handleToggle = (field: string) => { const exists = selectedColumns.find((col) => col.field === field); if (exists) { - onChange(selectedColumns.filter((col) => col.field !== field)); + onConfigChange({ columns: selectedColumns.filter((col) => col.field !== field) }); } else { const newCol: ListColumn = { id: `col_${selectedColumns.length}`, @@ -40,18 +40,22 @@ export function ColumnSelector({ availableColumns, selectedColumns, sampleData, align: "left", visible: true, }; - onChange([...selectedColumns, newCol]); + onConfigChange({ columns: [...selectedColumns, newCol] }); } }; // 컬럼 라벨 변경 const handleLabelChange = (field: string, label: string) => { - onChange(selectedColumns.map((col) => (col.field === field ? { ...col, label } : col))); + onConfigChange({ + columns: selectedColumns.map((col) => (col.field === field ? { ...col, label } : col)), + }); }; // 정렬 방향 변경 const handleAlignChange = (field: string, align: "left" | "center" | "right") => { - onChange(selectedColumns.map((col) => (col.field === field ? { ...col, align } : col))); + onConfigChange({ + columns: selectedColumns.map((col) => (col.field === field ? { ...col, align } : col)), + }); }; // 드래그 시작 @@ -64,40 +68,29 @@ export function ColumnSelector({ availableColumns, selectedColumns, sampleData, e.preventDefault(); if (draggedIndex === null || draggedIndex === hoverIndex) return; - setDragOverIndex(hoverIndex); - const newColumns = [...selectedColumns]; const draggedItem = newColumns[draggedIndex]; newColumns.splice(draggedIndex, 1); newColumns.splice(hoverIndex, 0, draggedItem); setDraggedIndex(hoverIndex); - onChange(newColumns); + onConfigChange({ columns: newColumns }); }; // 드롭 const handleDrop = (e: React.DragEvent) => { e.preventDefault(); setDraggedIndex(null); - setDragOverIndex(null); }; // 드래그 종료 const handleDragEnd = () => { setDraggedIndex(null); - setDragOverIndex(null); }; return ( - -
-

컬럼 선택 및 설정

-

- 표시할 컬럼을 선택하고 이름을 변경하세요. 드래그하여 순서를 변경할 수 있습니다. -

-
- -
+
+
{/* 선택된 컬럼을 먼저 순서대로 표시 */} {selectedColumns.map((selectedCol, columnIndex) => { const field = selectedCol.field; @@ -127,52 +120,74 @@ export function ColumnSelector({ availableColumns, selectedColumns, sampleData, handleDragEnd(); e.currentTarget.style.cursor = "grab"; }} - className={`rounded-lg border p-4 transition-all ${ - isSelected ? "border-blue-300 bg-blue-50" : "border-gray-200" + className={`group relative rounded-md border transition-all ${ + isSelected + ? "border-primary/40 bg-primary/5 shadow-sm" + : "border-gray-200 bg-white hover:border-gray-300 hover:shadow-sm" } ${isDraggable ? "cursor-grab active:cursor-grabbing" : ""} ${ - draggedIndex === columnIndex ? "opacity-50" : "" + draggedIndex === columnIndex ? "scale-95 opacity-50" : "" }`} > -
- handleToggle(field)} className="mt-1" /> -
-
- - {field} - {previewText && (예: {previewText})} + {/* 헤더 */} +
+ handleToggle(field)} + className="h-3.5 w-3.5 shrink-0" + /> + +
+
+ {field} + {previewText && 예: {previewText}}
+ {/* 설정 영역 */} {isSelected && selectedCol && ( -
- {/* 컬럼명 */} -
- - handleLabelChange(field, e.target.value)} - placeholder="컬럼명" - className="mt-1" - /> -
+
+
+ {/* 표시 이름 */} +
+ handleLabelChange(field, e.target.value)} + placeholder="표시 이름" + className="focus:border-primary focus:ring-primary/20 h-6 w-full border-gray-200 bg-white px-1.5 text-[10px] placeholder:text-gray-400 focus:ring-1" + style={{ fontSize: "10px" }} + /> +
- {/* 정렬 방향 */} -
- - + {/* 정렬 */} +
+ +
)} @@ -191,18 +206,23 @@ export function ColumnSelector({ availableColumns, selectedColumns, sampleData, ? JSON.stringify(preview).substring(0, 30) : String(preview).substring(0, 30) : ""; - const isSelected = false; - const isDraggable = false; return ( -
-
- handleToggle(field)} className="mt-1" /> -
-
- - {field} - {previewText && (예: {previewText})} +
+
+ handleToggle(field)} + className="h-3.5 w-3.5 shrink-0" + /> + +
+
+ {field} + {previewText && 예: {previewText}}
@@ -212,10 +232,11 @@ export function ColumnSelector({ availableColumns, selectedColumns, sampleData,
{selectedColumns.length === 0 && ( -
- ⚠️ 최소 1개 이상의 컬럼을 선택해주세요 +
+ ⚠️ + 최소 1개 이상의 컬럼을 선택해주세요
)} - +
); } diff --git a/frontend/components/admin/dashboard/widgets/list-widget/ListTableOptions.tsx b/frontend/components/admin/dashboard/widgets/list-widget/ListTableOptions.tsx index 870d1abf..5431a791 100644 --- a/frontend/components/admin/dashboard/widgets/list-widget/ListTableOptions.tsx +++ b/frontend/components/admin/dashboard/widgets/list-widget/ListTableOptions.tsx @@ -2,70 +2,42 @@ import React from "react"; import { ListWidgetConfig } from "../../types"; -import { Card } from "@/components/ui/card"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; -import { Checkbox } from "@/components/ui/checkbox"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { Input } from "@/components/ui/input"; interface ListTableOptionsProps { config: ListWidgetConfig; - onChange: (updates: Partial) => void; + onConfigChange: (updates: Partial) => void; } /** * 리스트 테이블 옵션 설정 컴포넌트 * - 페이지 크기, 검색, 정렬 등 설정 */ -export function ListTableOptions({ config, onChange }: ListTableOptionsProps) { +export function ListTableOptions({ config, onConfigChange }: ListTableOptionsProps) { return ( - -
-

테이블 옵션

-

테이블 동작과 스타일을 설정하세요

-
- -
+
+
{/* 뷰 모드 */}
- + onChange({ viewMode: value })} + onValueChange={(value: "table" | "card") => onConfigChange({ viewMode: value })} + className="flex items-center gap-3" > -
- -
- - {/* 컬럼 모드 */} -
- - onChange({ columnMode: value })} - > -
- - -
-
- -