리스트 위젯, 차트, 뷰어 수정사항 반영

This commit is contained in:
dohyeons 2025-10-20 15:43:17 +09:00
parent 84994a30e8
commit 3eac709478
4 changed files with 172 additions and 34 deletions

View File

@ -95,24 +95,21 @@ export function ListWidgetConfigModal({ isOpen, element, onClose, onSave }: List
}, []);
// 쿼리 실행 결과 처리
const handleQueryTest = useCallback(
(result: QueryResult) => {
setQueryResult(result);
const handleQueryTest = useCallback((result: QueryResult) => {
setQueryResult(result);
// 자동 모드이고 기존 컬럼이 없을 때만 자동 생성
if (listConfig.columnMode === "auto" && result.columns.length > 0 && listConfig.columns.length === 0) {
const autoColumns: ListColumn[] = result.columns.map((col, idx) => ({
id: `col_${idx}`,
label: col,
field: col,
align: "left",
visible: true,
}));
setListConfig((prev) => ({ ...prev, columns: autoColumns }));
}
},
[listConfig.columnMode, listConfig.columns.length],
);
// 쿼리 실행할 때마다 컬럼 초기화 후 자동 생성
if (result.columns.length > 0) {
const autoColumns: ListColumn[] = result.columns.map((col, idx) => ({
id: `col_${idx}`,
label: col,
field: col,
align: "left",
visible: true,
}));
setListConfig((prev) => ({ ...prev, columns: autoColumns }));
}
}, []);
// 다음 단계
const handleNext = () => {
@ -176,9 +173,7 @@ export function ListWidgetConfigModal({ isOpen, element, onClose, onSave }: List
</div>
{/* 참고: 리스트 위젯은 제목이 항상 표시됩니다 */}
<div className="rounded bg-blue-50 p-2 text-xs text-blue-700">
💡
</div>
<div className="rounded bg-blue-50 p-2 text-xs text-blue-700">💡 </div>
</div>
{/* 진행 상태 표시 */}

View File

@ -1,6 +1,6 @@
"use client";
import React from "react";
import React, { useState } from "react";
import { ListColumn } from "../../types";
import { Card } from "@/components/ui/card";
import { Label } from "@/components/ui/label";
@ -21,8 +21,12 @@ interface ColumnSelectorProps {
* -
* -
* - , ,
* -
*/
export function ColumnSelector({ availableColumns, selectedColumns, sampleData, onChange }: ColumnSelectorProps) {
const [draggedIndex, setDraggedIndex] = useState<number | null>(null);
const [dragOverIndex, setDragOverIndex] = useState<number | null>(null);
// 컬럼 선택/해제
const handleToggle = (field: string) => {
const exists = selectedColumns.find((col) => col.field === field);
@ -50,17 +54,53 @@ export function ColumnSelector({ availableColumns, selectedColumns, sampleData,
onChange(selectedColumns.map((col) => (col.field === field ? { ...col, align } : col)));
};
// 드래그 시작
const handleDragStart = (index: number) => {
setDraggedIndex(index);
};
// 드래그 오버 - 실시간으로 순서 변경하여 UI 업데이트
const handleDragOver = (e: React.DragEvent, hoverIndex: number) => {
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);
};
// 드롭
const handleDrop = (e: React.DragEvent) => {
e.preventDefault();
setDraggedIndex(null);
setDragOverIndex(null);
};
// 드래그 종료
const handleDragEnd = () => {
setDraggedIndex(null);
setDragOverIndex(null);
};
return (
<Card className="p-4">
<div className="mb-4">
<h3 className="text-lg font-semibold text-gray-800"> </h3>
<p className="text-sm text-gray-600"> </p>
<p className="text-sm text-gray-600">
. .
</p>
</div>
<div className="space-y-3">
{availableColumns.map((field) => {
const selectedCol = selectedColumns.find((col) => col.field === field);
const isSelected = !!selectedCol;
{/* 선택된 컬럼을 먼저 순서대로 표시 */}
{selectedColumns.map((selectedCol, columnIndex) => {
const field = selectedCol.field;
const preview = sampleData[field];
const previewText =
preview !== undefined && preview !== null
@ -68,19 +108,36 @@ export function ColumnSelector({ availableColumns, selectedColumns, sampleData,
? JSON.stringify(preview).substring(0, 30)
: String(preview).substring(0, 30)
: "";
const isSelected = true;
const isDraggable = true;
return (
<div
key={field}
className={`rounded-lg border p-4 transition-colors ${
draggable={isDraggable}
onDragStart={(e) => {
if (isDraggable) {
handleDragStart(columnIndex);
e.currentTarget.style.cursor = "grabbing";
}
}}
onDragOver={(e) => isDraggable && handleDragOver(e, columnIndex)}
onDrop={handleDrop}
onDragEnd={(e) => {
handleDragEnd();
e.currentTarget.style.cursor = "grab";
}}
className={`rounded-lg border p-4 transition-all ${
isSelected ? "border-blue-300 bg-blue-50" : "border-gray-200"
} ${isDraggable ? "cursor-grab active:cursor-grabbing" : ""} ${
draggedIndex === columnIndex ? "opacity-50" : ""
}`}
>
<div className="mb-3 flex items-start gap-3">
<Checkbox checked={isSelected} onCheckedChange={() => handleToggle(field)} className="mt-1" />
<div className="flex-1">
<div className="flex items-center gap-2">
<GripVertical className="h-4 w-4 text-gray-400" />
<GripVertical className={`h-4 w-4 ${isDraggable ? "text-blue-500" : "text-gray-400"}`} />
<span className="font-medium text-gray-700">{field}</span>
{previewText && <span className="text-xs text-gray-500">(: {previewText})</span>}
</div>
@ -122,6 +179,36 @@ export function ColumnSelector({ availableColumns, selectedColumns, sampleData,
</div>
);
})}
{/* 선택되지 않은 컬럼들을 아래에 표시 */}
{availableColumns
.filter((field) => !selectedColumns.find((col) => col.field === field))
.map((field) => {
const preview = sampleData[field];
const previewText =
preview !== undefined && preview !== null
? typeof preview === "object"
? JSON.stringify(preview).substring(0, 30)
: String(preview).substring(0, 30)
: "";
const isSelected = false;
const isDraggable = false;
return (
<div key={field} className={`rounded-lg border border-gray-200 p-4 transition-all`}>
<div className="mb-3 flex items-start gap-3">
<Checkbox checked={false} onCheckedChange={() => handleToggle(field)} className="mt-1" />
<div className="flex-1">
<div className="flex items-center gap-2">
<GripVertical className="h-4 w-4 text-gray-400" />
<span className="font-medium text-gray-700">{field}</span>
{previewText && <span className="text-xs text-gray-500">(: {previewText})</span>}
</div>
</div>
</div>
</div>
);
})}
</div>
{selectedColumns.length === 0 && (

View File

@ -1,6 +1,6 @@
"use client";
import React from "react";
import React, { useState } from "react";
import { ListColumn } from "../../types";
import { Card } from "@/components/ui/card";
import { Label } from "@/components/ui/label";
@ -19,8 +19,12 @@ interface ManualColumnEditorProps {
*
* - /
* -
* -
*/
export function ManualColumnEditor({ availableFields, columns, onChange }: ManualColumnEditorProps) {
const [draggedIndex, setDraggedIndex] = useState<number | null>(null);
const [dragOverIndex, setDragOverIndex] = useState<number | null>(null);
// 새 컬럼 추가
const handleAddColumn = () => {
const newCol: ListColumn = {
@ -43,12 +47,48 @@ export function ManualColumnEditor({ availableFields, columns, onChange }: Manua
onChange(columns.map((col) => (col.id === id ? { ...col, ...updates } : col)));
};
// 드래그 시작
const handleDragStart = (index: number) => {
setDraggedIndex(index);
};
// 드래그 오버 - 실시간으로 순서 변경하여 UI 업데이트
const handleDragOver = (e: React.DragEvent, hoverIndex: number) => {
e.preventDefault();
if (draggedIndex === null || draggedIndex === hoverIndex) return;
setDragOverIndex(hoverIndex);
const newColumns = [...columns];
const draggedItem = newColumns[draggedIndex];
newColumns.splice(draggedIndex, 1);
newColumns.splice(hoverIndex, 0, draggedItem);
setDraggedIndex(hoverIndex);
onChange(newColumns);
};
// 드롭
const handleDrop = (e: React.DragEvent) => {
e.preventDefault();
setDraggedIndex(null);
setDragOverIndex(null);
};
// 드래그 종료
const handleDragEnd = () => {
setDraggedIndex(null);
setDragOverIndex(null);
};
return (
<Card className="p-4">
<div className="mb-4 flex items-center justify-between">
<div>
<h3 className="text-lg font-semibold text-gray-800"> </h3>
<p className="text-sm text-gray-600"> </p>
<p className="text-sm text-gray-600">
. .
</p>
</div>
<Button onClick={handleAddColumn} size="sm" className="gap-2">
<Plus className="h-4 w-4" />
@ -58,9 +98,25 @@ export function ManualColumnEditor({ availableFields, columns, onChange }: Manua
<div className="space-y-3">
{columns.map((col, index) => (
<div key={col.id} className="rounded-lg border border-gray-200 bg-gray-50 p-4">
<div
key={col.id}
draggable
onDragStart={(e) => {
handleDragStart(index);
e.currentTarget.style.cursor = "grabbing";
}}
onDragOver={(e) => handleDragOver(e, index)}
onDrop={handleDrop}
onDragEnd={(e) => {
handleDragEnd();
e.currentTarget.style.cursor = "grab";
}}
className={`cursor-grab rounded-lg border border-gray-200 bg-gray-50 p-4 transition-all active:cursor-grabbing ${
draggedIndex === index ? "opacity-50" : ""
}`}
>
<div className="mb-3 flex items-center gap-2">
<GripVertical className="h-4 w-4 text-gray-400" />
<GripVertical className="h-4 w-4 text-blue-500" />
<span className="font-medium text-gray-700"> {index + 1}</span>
<Button
onClick={() => handleRemove(col.id)}

View File

@ -278,11 +278,11 @@ export function DashboardViewer({
return (
<DashboardProvider>
{/* overflow-auto 제거 - 외부 페이지 스크롤 사용 */}
<div className="flex h-full items-start justify-center bg-gray-100 p-8">
{/* 스크롤 가능한 컨테이너 */}
<div className="flex min-h-screen items-start justify-center bg-gray-100 p-8">
{/* 고정 크기 캔버스 (편집 화면과 동일한 레이아웃) */}
<div
className="relative overflow-hidden rounded-lg"
className="relative rounded-lg"
style={{
width: `${canvasConfig.width}px`,
minHeight: `${canvasConfig.height}px`,