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

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

View File

@ -1,6 +1,6 @@
"use client"; "use client";
import React from "react"; import React, { useState } from "react";
import { ListColumn } from "../../types"; import { ListColumn } from "../../types";
import { Card } from "@/components/ui/card"; import { Card } from "@/components/ui/card";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
@ -21,8 +21,12 @@ interface ColumnSelectorProps {
* - * -
* - * -
* - , , * - , ,
* -
*/ */
export function ColumnSelector({ availableColumns, selectedColumns, sampleData, onChange }: 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 handleToggle = (field: string) => {
const exists = selectedColumns.find((col) => col.field === field); 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))); 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 ( return (
<Card className="p-4"> <Card className="p-4">
<div className="mb-4"> <div className="mb-4">
<h3 className="text-lg font-semibold text-gray-800"> </h3> <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>
<div className="space-y-3"> <div className="space-y-3">
{availableColumns.map((field) => { {/* 선택된 컬럼을 먼저 순서대로 표시 */}
const selectedCol = selectedColumns.find((col) => col.field === field); {selectedColumns.map((selectedCol, columnIndex) => {
const isSelected = !!selectedCol; const field = selectedCol.field;
const preview = sampleData[field]; const preview = sampleData[field];
const previewText = const previewText =
preview !== undefined && preview !== null preview !== undefined && preview !== null
@ -68,19 +108,36 @@ export function ColumnSelector({ availableColumns, selectedColumns, sampleData,
? JSON.stringify(preview).substring(0, 30) ? JSON.stringify(preview).substring(0, 30)
: String(preview).substring(0, 30) : String(preview).substring(0, 30)
: ""; : "";
const isSelected = true;
const isDraggable = true;
return ( return (
<div <div
key={field} 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" 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"> <div className="mb-3 flex items-start gap-3">
<Checkbox checked={isSelected} onCheckedChange={() => handleToggle(field)} className="mt-1" /> <Checkbox checked={isSelected} onCheckedChange={() => handleToggle(field)} className="mt-1" />
<div className="flex-1"> <div className="flex-1">
<div className="flex items-center gap-2"> <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> <span className="font-medium text-gray-700">{field}</span>
{previewText && <span className="text-xs text-gray-500">(: {previewText})</span>} {previewText && <span className="text-xs text-gray-500">(: {previewText})</span>}
</div> </div>
@ -122,6 +179,36 @@ export function ColumnSelector({ availableColumns, selectedColumns, sampleData,
</div> </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> </div>
{selectedColumns.length === 0 && ( {selectedColumns.length === 0 && (

View File

@ -1,6 +1,6 @@
"use client"; "use client";
import React from "react"; import React, { useState } from "react";
import { ListColumn } from "../../types"; import { ListColumn } from "../../types";
import { Card } from "@/components/ui/card"; import { Card } from "@/components/ui/card";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
@ -19,8 +19,12 @@ interface ManualColumnEditorProps {
* *
* - / * - /
* - * -
* -
*/ */
export function ManualColumnEditor({ availableFields, columns, onChange }: 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 handleAddColumn = () => {
const newCol: ListColumn = { const newCol: ListColumn = {
@ -43,12 +47,48 @@ export function ManualColumnEditor({ availableFields, columns, onChange }: Manua
onChange(columns.map((col) => (col.id === id ? { ...col, ...updates } : col))); 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 ( return (
<Card className="p-4"> <Card className="p-4">
<div className="mb-4 flex items-center justify-between"> <div className="mb-4 flex items-center justify-between">
<div> <div>
<h3 className="text-lg font-semibold text-gray-800"> </h3> <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>
<Button onClick={handleAddColumn} size="sm" className="gap-2"> <Button onClick={handleAddColumn} size="sm" className="gap-2">
<Plus className="h-4 w-4" /> <Plus className="h-4 w-4" />
@ -58,9 +98,25 @@ export function ManualColumnEditor({ availableFields, columns, onChange }: Manua
<div className="space-y-3"> <div className="space-y-3">
{columns.map((col, index) => ( {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"> <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> <span className="font-medium text-gray-700"> {index + 1}</span>
<Button <Button
onClick={() => handleRemove(col.id)} onClick={() => handleRemove(col.id)}

View File

@ -278,11 +278,11 @@ export function DashboardViewer({
return ( return (
<DashboardProvider> <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 <div
className="relative overflow-hidden rounded-lg" className="relative rounded-lg"
style={{ style={{
width: `${canvasConfig.width}px`, width: `${canvasConfig.width}px`,
minHeight: `${canvasConfig.height}px`, minHeight: `${canvasConfig.height}px`,