669 lines
32 KiB
TypeScript
669 lines
32 KiB
TypeScript
"use client";
|
|
|
|
import React, { useState } from "react";
|
|
import { ListWidgetConfig, QueryResult, FieldGroup, FieldConfig, DisplayColumnConfig } from "../types";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Switch } from "@/components/ui/switch";
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
import { UnifiedColumnEditor } from "../widgets/list-widget/UnifiedColumnEditor";
|
|
import { ListTableOptions } from "../widgets/list-widget/ListTableOptions";
|
|
import { Plus, Trash2, ChevronDown, ChevronUp, X, Check } from "lucide-react";
|
|
import { Checkbox } from "@/components/ui/checkbox";
|
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
|
|
|
interface ListWidgetSectionProps {
|
|
queryResult: QueryResult | null;
|
|
config: ListWidgetConfig;
|
|
onConfigChange: (updates: Partial<ListWidgetConfig>) => void;
|
|
}
|
|
|
|
/**
|
|
* 리스트 위젯 설정 섹션
|
|
* - 컬럼 설정
|
|
* - 테이블 옵션
|
|
* - 행 클릭 팝업 설정
|
|
*/
|
|
export function ListWidgetSection({ queryResult, config, onConfigChange }: ListWidgetSectionProps) {
|
|
const [expandedGroups, setExpandedGroups] = useState<Record<string, boolean>>({});
|
|
|
|
// 팝업 설정 초기화
|
|
const popupConfig = config.rowDetailPopup || {
|
|
enabled: false,
|
|
title: "상세 정보",
|
|
additionalQuery: { enabled: false, tableName: "", matchColumn: "" },
|
|
fieldGroups: [],
|
|
};
|
|
|
|
// 팝업 설정 업데이트 헬퍼
|
|
const updatePopupConfig = (updates: Partial<typeof popupConfig>) => {
|
|
onConfigChange({
|
|
rowDetailPopup: { ...popupConfig, ...updates },
|
|
});
|
|
};
|
|
|
|
// 필드 그룹 추가
|
|
const addFieldGroup = () => {
|
|
const newGroup: FieldGroup = {
|
|
id: `group-${Date.now()}`,
|
|
title: "새 그룹",
|
|
icon: "info",
|
|
color: "gray",
|
|
fields: [],
|
|
};
|
|
updatePopupConfig({
|
|
fieldGroups: [...(popupConfig.fieldGroups || []), newGroup],
|
|
});
|
|
};
|
|
|
|
// 필드 그룹 삭제
|
|
const removeFieldGroup = (groupId: string) => {
|
|
updatePopupConfig({
|
|
fieldGroups: (popupConfig.fieldGroups || []).filter((g) => g.id !== groupId),
|
|
});
|
|
};
|
|
|
|
// 필드 그룹 업데이트
|
|
const updateFieldGroup = (groupId: string, updates: Partial<FieldGroup>) => {
|
|
updatePopupConfig({
|
|
fieldGroups: (popupConfig.fieldGroups || []).map((g) => (g.id === groupId ? { ...g, ...updates } : g)),
|
|
});
|
|
};
|
|
|
|
// 필드 추가
|
|
const addField = (groupId: string) => {
|
|
const newField: FieldConfig = {
|
|
column: "",
|
|
label: "",
|
|
format: "text",
|
|
};
|
|
updatePopupConfig({
|
|
fieldGroups: (popupConfig.fieldGroups || []).map((g) =>
|
|
g.id === groupId ? { ...g, fields: [...g.fields, newField] } : g,
|
|
),
|
|
});
|
|
};
|
|
|
|
// 필드 삭제
|
|
const removeField = (groupId: string, fieldIndex: number) => {
|
|
updatePopupConfig({
|
|
fieldGroups: (popupConfig.fieldGroups || []).map((g) =>
|
|
g.id === groupId ? { ...g, fields: g.fields.filter((_, i) => i !== fieldIndex) } : g,
|
|
),
|
|
});
|
|
};
|
|
|
|
// 필드 업데이트
|
|
const updateField = (groupId: string, fieldIndex: number, updates: Partial<FieldConfig>) => {
|
|
updatePopupConfig({
|
|
fieldGroups: (popupConfig.fieldGroups || []).map((g) =>
|
|
g.id === groupId ? { ...g, fields: g.fields.map((f, i) => (i === fieldIndex ? { ...f, ...updates } : f)) } : g,
|
|
),
|
|
});
|
|
};
|
|
|
|
// 그룹 확장/축소 토글
|
|
const toggleGroupExpand = (groupId: string) => {
|
|
setExpandedGroups((prev) => ({ ...prev, [groupId]: !prev[groupId] }));
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-3">
|
|
{/* 컬럼 설정 - 쿼리 실행 후에만 표시 */}
|
|
{queryResult && queryResult.columns.length > 0 && (
|
|
<div className="rounded-lg bg-background p-3 shadow-sm">
|
|
<Label className="mb-2 block text-xs font-semibold">컬럼 설정</Label>
|
|
<UnifiedColumnEditor queryResult={queryResult} config={config} onConfigChange={onConfigChange} />
|
|
</div>
|
|
)}
|
|
|
|
{/* 테이블 옵션 */}
|
|
{config.columns.length > 0 && (
|
|
<div className="rounded-lg bg-background p-3 shadow-sm">
|
|
<Label className="mb-2 block text-xs font-semibold">테이블 옵션</Label>
|
|
<ListTableOptions config={config} onConfigChange={onConfigChange} />
|
|
</div>
|
|
)}
|
|
|
|
{/* 행 클릭 팝업 설정 */}
|
|
<div className="rounded-lg bg-background p-3 shadow-sm">
|
|
<div className="mb-3 flex items-center justify-between">
|
|
<Label className="text-xs font-semibold">행 클릭 팝업</Label>
|
|
<Switch
|
|
checked={popupConfig.enabled}
|
|
onCheckedChange={(enabled) => updatePopupConfig({ enabled })}
|
|
aria-label="행 클릭 팝업 활성화"
|
|
/>
|
|
</div>
|
|
|
|
{popupConfig.enabled && (
|
|
<div className="space-y-3">
|
|
{/* 팝업 제목 */}
|
|
<div>
|
|
<Label className="text-xs">팝업 제목</Label>
|
|
<Input
|
|
value={popupConfig.title || ""}
|
|
onChange={(e) => updatePopupConfig({ title: e.target.value })}
|
|
placeholder="상세 정보"
|
|
className="mt-1 h-8 text-xs"
|
|
/>
|
|
</div>
|
|
|
|
{/* 추가 데이터 조회 설정 */}
|
|
<div className="space-y-2 rounded border p-2">
|
|
<div className="flex items-center justify-between">
|
|
<Label className="text-xs">추가 데이터 조회</Label>
|
|
<Switch
|
|
checked={popupConfig.additionalQuery?.enabled || false}
|
|
onCheckedChange={(enabled) =>
|
|
updatePopupConfig({
|
|
additionalQuery: { ...popupConfig.additionalQuery, enabled, queryMode: "table", tableName: "", matchColumn: "" },
|
|
})
|
|
}
|
|
aria-label="추가 데이터 조회 활성화"
|
|
/>
|
|
</div>
|
|
|
|
{popupConfig.additionalQuery?.enabled && (
|
|
<div className="space-y-2">
|
|
{/* 조회 모드 선택 */}
|
|
<div>
|
|
<Label className="text-xs">조회 모드</Label>
|
|
<Select
|
|
value={popupConfig.additionalQuery?.queryMode || "table"}
|
|
onValueChange={(value: "table" | "custom") =>
|
|
updatePopupConfig({
|
|
additionalQuery: { ...popupConfig.additionalQuery!, queryMode: value },
|
|
})
|
|
}
|
|
>
|
|
<SelectTrigger className="mt-1 h-8 text-xs">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="table">테이블 조회</SelectItem>
|
|
<SelectItem value="custom">커스텀 쿼리</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{/* 테이블 조회 모드 */}
|
|
{(popupConfig.additionalQuery?.queryMode || "table") === "table" && (
|
|
<>
|
|
<div>
|
|
<Label className="text-xs">테이블명</Label>
|
|
<Input
|
|
value={popupConfig.additionalQuery?.tableName || ""}
|
|
onChange={(e) =>
|
|
updatePopupConfig({
|
|
additionalQuery: { ...popupConfig.additionalQuery!, tableName: e.target.value },
|
|
})
|
|
}
|
|
placeholder="vehicles"
|
|
className="mt-1 h-8 text-xs"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label className="text-xs">매칭 컬럼 (조회 테이블)</Label>
|
|
<Input
|
|
value={popupConfig.additionalQuery?.matchColumn || ""}
|
|
onChange={(e) =>
|
|
updatePopupConfig({
|
|
additionalQuery: { ...popupConfig.additionalQuery!, matchColumn: e.target.value },
|
|
})
|
|
}
|
|
placeholder="id"
|
|
className="mt-1 h-8 text-xs"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label className="text-xs">소스 컬럼 (클릭한 행)</Label>
|
|
<Input
|
|
value={popupConfig.additionalQuery?.sourceColumn || ""}
|
|
onChange={(e) =>
|
|
updatePopupConfig({
|
|
additionalQuery: { ...popupConfig.additionalQuery!, sourceColumn: e.target.value },
|
|
})
|
|
}
|
|
placeholder="비워두면 매칭 컬럼과 동일"
|
|
className="mt-1 h-8 text-xs"
|
|
/>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{/* 커스텀 쿼리 모드 */}
|
|
{popupConfig.additionalQuery?.queryMode === "custom" && (
|
|
<>
|
|
<div>
|
|
<Label className="text-xs">소스 컬럼 (클릭한 행)</Label>
|
|
<Input
|
|
value={popupConfig.additionalQuery?.sourceColumn || ""}
|
|
onChange={(e) =>
|
|
updatePopupConfig({
|
|
additionalQuery: { ...popupConfig.additionalQuery!, sourceColumn: e.target.value },
|
|
})
|
|
}
|
|
placeholder="id"
|
|
className="mt-1 h-8 text-xs"
|
|
/>
|
|
<p className="text-muted-foreground mt-1 text-xs">쿼리에서 사용할 파라미터 컬럼</p>
|
|
</div>
|
|
<div>
|
|
<Label className="text-xs">커스텀 쿼리</Label>
|
|
<textarea
|
|
value={popupConfig.additionalQuery?.customQuery || ""}
|
|
onChange={(e) =>
|
|
updatePopupConfig({
|
|
additionalQuery: { ...popupConfig.additionalQuery!, customQuery: e.target.value },
|
|
})
|
|
}
|
|
placeholder={`SELECT
|
|
v.vehicle_number AS "차량번호",
|
|
ROUND(SUM(ts.loaded_distance_km)::NUMERIC, 2) AS "운행거리"
|
|
FROM vehicles v
|
|
LEFT JOIN transport_statistics ts ON v.id = ts.vehicle_id
|
|
WHERE v.id = {id}
|
|
GROUP BY v.id;`}
|
|
className="mt-1 h-32 w-full rounded-md border border-input bg-background px-3 py-2 text-xs font-mono ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
/>
|
|
<p className="text-muted-foreground mt-1 text-xs">
|
|
{"{id}"}, {"{vehicle_number}"} 등 클릭한 행의 컬럼값을 파라미터로 사용
|
|
</p>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{/* 표시할 컬럼 선택 - 테이블 모드와 커스텀 쿼리 모드 분기 */}
|
|
<div>
|
|
<Label className="text-xs">표시할 컬럼 선택</Label>
|
|
|
|
{/* 테이블 모드: 기존 쿼리 결과에서 선택 */}
|
|
{popupConfig.additionalQuery?.queryMode !== "custom" && (
|
|
<>
|
|
<Popover>
|
|
<PopoverTrigger asChild>
|
|
<Button variant="outline" className="mt-1 h-8 w-full justify-between text-xs">
|
|
<span className="truncate">
|
|
{(popupConfig.additionalQuery?.displayColumns?.length || 0) > 0
|
|
? `${popupConfig.additionalQuery?.displayColumns?.length}개 선택됨`
|
|
: "전체 표시 (클릭하여 선택)"}
|
|
</span>
|
|
<ChevronDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent className="w-72 p-2" align="start">
|
|
<div className="mb-2 flex items-center justify-between">
|
|
<span className="text-xs font-medium">컬럼 선택</span>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="h-6 text-xs"
|
|
onClick={() =>
|
|
updatePopupConfig({
|
|
additionalQuery: { ...popupConfig.additionalQuery!, displayColumns: [] },
|
|
})
|
|
}
|
|
>
|
|
초기화
|
|
</Button>
|
|
</div>
|
|
<div className="max-h-48 space-y-1 overflow-y-auto">
|
|
{/* 쿼리 결과 컬럼 목록 */}
|
|
{queryResult?.columns.map((col) => {
|
|
const currentColumns = popupConfig.additionalQuery?.displayColumns || [];
|
|
const existingConfig = currentColumns.find((c) =>
|
|
typeof c === 'object' ? c.column === col : c === col
|
|
);
|
|
const isSelected = !!existingConfig;
|
|
return (
|
|
<div
|
|
key={col}
|
|
className="flex cursor-pointer items-center gap-2 rounded px-2 py-1 hover:bg-muted"
|
|
onClick={() => {
|
|
const newColumns = isSelected
|
|
? currentColumns.filter((c) =>
|
|
typeof c === 'object' ? c.column !== col : c !== col
|
|
)
|
|
: [...currentColumns, { column: col, label: col } as DisplayColumnConfig];
|
|
updatePopupConfig({
|
|
additionalQuery: { ...popupConfig.additionalQuery!, displayColumns: newColumns },
|
|
});
|
|
}}
|
|
>
|
|
<Checkbox checked={isSelected} className="h-3 w-3" />
|
|
<span className="text-xs">{col}</span>
|
|
</div>
|
|
);
|
|
})}
|
|
{(!queryResult?.columns || queryResult.columns.length === 0) && (
|
|
<p className="text-muted-foreground py-2 text-center text-xs">
|
|
쿼리를 먼저 실행해주세요
|
|
</p>
|
|
)}
|
|
</div>
|
|
</PopoverContent>
|
|
</Popover>
|
|
<p className="text-muted-foreground mt-1 text-xs">비워두면 모든 컬럼이 표시됩니다</p>
|
|
</>
|
|
)}
|
|
|
|
{/* 커스텀 쿼리 모드: 직접 입력 방식 */}
|
|
{popupConfig.additionalQuery?.queryMode === "custom" && (
|
|
<>
|
|
<p className="text-muted-foreground mt-1 text-xs">
|
|
커스텀 쿼리의 결과 컬럼이 자동으로 표시됩니다.
|
|
쿼리에서 AS "라벨명" 형태로 alias를 지정하면 해당 라벨로 표시됩니다.
|
|
</p>
|
|
<div className="mt-2 flex items-center gap-2">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className="h-7 gap-1 text-xs"
|
|
onClick={() => {
|
|
const newColumns = [...(popupConfig.additionalQuery?.displayColumns || []), { column: "", label: "" }];
|
|
updatePopupConfig({
|
|
additionalQuery: { ...popupConfig.additionalQuery!, displayColumns: newColumns },
|
|
});
|
|
}}
|
|
>
|
|
<Plus className="h-3 w-3" />
|
|
컬럼 추가 (선택사항)
|
|
</Button>
|
|
{(popupConfig.additionalQuery?.displayColumns?.length || 0) > 0 && (
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="h-7 text-xs"
|
|
onClick={() =>
|
|
updatePopupConfig({
|
|
additionalQuery: { ...popupConfig.additionalQuery!, displayColumns: [] },
|
|
})
|
|
}
|
|
>
|
|
초기화
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{/* 선택된 컬럼 라벨 편집 (테이블 모드) */}
|
|
{popupConfig.additionalQuery?.queryMode !== "custom" && (popupConfig.additionalQuery?.displayColumns?.length || 0) > 0 && (
|
|
<div className="mt-3 space-y-2">
|
|
<Label className="text-xs">컬럼 라벨 설정</Label>
|
|
<div className="space-y-1.5">
|
|
{popupConfig.additionalQuery?.displayColumns?.map((colConfig, index) => {
|
|
const column = typeof colConfig === 'object' ? colConfig.column : colConfig;
|
|
const label = typeof colConfig === 'object' ? colConfig.label : colConfig;
|
|
return (
|
|
<div key={column} className="flex items-center gap-2">
|
|
<span className="text-muted-foreground w-24 truncate text-xs" title={column}>
|
|
{column}
|
|
</span>
|
|
<Input
|
|
value={label}
|
|
onChange={(e) => {
|
|
const newColumns = [...(popupConfig.additionalQuery?.displayColumns || [])];
|
|
newColumns[index] = { column, label: e.target.value };
|
|
updatePopupConfig({
|
|
additionalQuery: { ...popupConfig.additionalQuery!, displayColumns: newColumns },
|
|
});
|
|
}}
|
|
placeholder="표시 라벨"
|
|
className="h-7 flex-1 text-xs"
|
|
/>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="h-7 w-7 p-0"
|
|
onClick={() => {
|
|
const newColumns = (popupConfig.additionalQuery?.displayColumns || []).filter(
|
|
(c) => (typeof c === 'object' ? c.column : c) !== column
|
|
);
|
|
updatePopupConfig({
|
|
additionalQuery: { ...popupConfig.additionalQuery!, displayColumns: newColumns },
|
|
});
|
|
}}
|
|
>
|
|
<X className="h-3 w-3" />
|
|
</Button>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* 커스텀 쿼리 모드: 직접 입력 컬럼 편집 */}
|
|
{popupConfig.additionalQuery?.queryMode === "custom" && (popupConfig.additionalQuery?.displayColumns?.length || 0) > 0 && (
|
|
<div className="mt-3 space-y-2">
|
|
<Label className="text-xs">표시할 컬럼 직접 입력</Label>
|
|
<p className="text-muted-foreground text-xs">커스텀 쿼리 결과의 컬럼명을 직접 입력하세요</p>
|
|
<div className="space-y-1.5">
|
|
{popupConfig.additionalQuery?.displayColumns?.map((colConfig, index) => {
|
|
const column = typeof colConfig === 'object' ? colConfig.column : colConfig;
|
|
const label = typeof colConfig === 'object' ? colConfig.label : colConfig;
|
|
return (
|
|
<div key={index} className="flex items-center gap-2">
|
|
<Input
|
|
value={column}
|
|
onChange={(e) => {
|
|
const newColumns = [...(popupConfig.additionalQuery?.displayColumns || [])];
|
|
newColumns[index] = { column: e.target.value, label: label || e.target.value };
|
|
updatePopupConfig({
|
|
additionalQuery: { ...popupConfig.additionalQuery!, displayColumns: newColumns },
|
|
});
|
|
}}
|
|
placeholder="컬럼명 (쿼리 결과)"
|
|
className="h-7 flex-1 text-xs"
|
|
/>
|
|
<Input
|
|
value={label}
|
|
onChange={(e) => {
|
|
const newColumns = [...(popupConfig.additionalQuery?.displayColumns || [])];
|
|
newColumns[index] = { column, label: e.target.value };
|
|
updatePopupConfig({
|
|
additionalQuery: { ...popupConfig.additionalQuery!, displayColumns: newColumns },
|
|
});
|
|
}}
|
|
placeholder="표시 라벨"
|
|
className="h-7 flex-1 text-xs"
|
|
/>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="h-7 w-7 p-0"
|
|
onClick={() => {
|
|
const newColumns = (popupConfig.additionalQuery?.displayColumns || []).filter(
|
|
(_, i) => i !== index
|
|
);
|
|
updatePopupConfig({
|
|
additionalQuery: { ...popupConfig.additionalQuery!, displayColumns: newColumns },
|
|
});
|
|
}}
|
|
>
|
|
<X className="h-3 w-3" />
|
|
</Button>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* 필드 그룹 설정 */}
|
|
<div className="space-y-2">
|
|
<div className="flex items-center justify-between">
|
|
<Label className="text-xs">필드 그룹 (선택사항)</Label>
|
|
<Button variant="outline" size="sm" onClick={addFieldGroup} className="h-7 gap-1 text-xs">
|
|
<Plus className="h-3 w-3" />
|
|
그룹 추가
|
|
</Button>
|
|
</div>
|
|
<p className="text-muted-foreground text-xs">설정하지 않으면 모든 필드가 자동으로 표시됩니다.</p>
|
|
|
|
{/* 필드 그룹 목록 */}
|
|
{(popupConfig.fieldGroups || []).map((group) => (
|
|
<div key={group.id} className="rounded border p-2">
|
|
{/* 그룹 헤더 */}
|
|
<div className="flex items-center justify-between">
|
|
<button
|
|
onClick={() => toggleGroupExpand(group.id)}
|
|
className="flex flex-1 items-center gap-2 text-left"
|
|
>
|
|
{expandedGroups[group.id] ? (
|
|
<ChevronUp className="h-3 w-3" />
|
|
) : (
|
|
<ChevronDown className="h-3 w-3" />
|
|
)}
|
|
<span className="text-xs font-medium">{group.title || "새 그룹"}</span>
|
|
<span className="text-muted-foreground text-xs">({group.fields.length}개 필드)</span>
|
|
</button>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => removeFieldGroup(group.id)}
|
|
className="h-6 w-6 p-0 text-destructive"
|
|
>
|
|
<Trash2 className="h-3 w-3" />
|
|
</Button>
|
|
</div>
|
|
|
|
{/* 그룹 상세 (확장 시) */}
|
|
{expandedGroups[group.id] && (
|
|
<div className="mt-2 space-y-2 border-t pt-2">
|
|
{/* 그룹 제목 */}
|
|
<div className="grid grid-cols-2 gap-2">
|
|
<div>
|
|
<Label className="text-xs">제목</Label>
|
|
<Input
|
|
value={group.title}
|
|
onChange={(e) => updateFieldGroup(group.id, { title: e.target.value })}
|
|
className="mt-1 h-7 text-xs"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label className="text-xs">색상</Label>
|
|
<Select
|
|
value={group.color || "gray"}
|
|
onValueChange={(value) =>
|
|
updateFieldGroup(group.id, {
|
|
color: value as "blue" | "orange" | "green" | "red" | "purple" | "gray",
|
|
})
|
|
}
|
|
>
|
|
<SelectTrigger className="mt-1 h-7 text-xs">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="gray">회색</SelectItem>
|
|
<SelectItem value="blue">파랑</SelectItem>
|
|
<SelectItem value="orange">주황</SelectItem>
|
|
<SelectItem value="green">초록</SelectItem>
|
|
<SelectItem value="red">빨강</SelectItem>
|
|
<SelectItem value="purple">보라</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 아이콘 */}
|
|
<div>
|
|
<Label className="text-xs">아이콘</Label>
|
|
<Select
|
|
value={group.icon || "info"}
|
|
onValueChange={(value) => updateFieldGroup(group.id, { icon: value })}
|
|
>
|
|
<SelectTrigger className="mt-1 h-7 text-xs">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="info">정보</SelectItem>
|
|
<SelectItem value="truck">트럭</SelectItem>
|
|
<SelectItem value="clock">시계</SelectItem>
|
|
<SelectItem value="map">지도</SelectItem>
|
|
<SelectItem value="package">박스</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{/* 필드 목록 */}
|
|
<div className="space-y-2">
|
|
<div className="flex items-center justify-between">
|
|
<Label className="text-xs">필드</Label>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => addField(group.id)}
|
|
className="h-6 gap-1 text-xs"
|
|
>
|
|
<Plus className="h-3 w-3" />
|
|
필드 추가
|
|
</Button>
|
|
</div>
|
|
|
|
{group.fields.map((field, fieldIndex) => (
|
|
<div key={fieldIndex} className="flex items-center gap-1 rounded bg-muted/50 p-1">
|
|
<Input
|
|
value={field.column}
|
|
onChange={(e) => updateField(group.id, fieldIndex, { column: e.target.value })}
|
|
placeholder="컬럼명"
|
|
className="h-6 flex-1 text-xs"
|
|
/>
|
|
<Input
|
|
value={field.label}
|
|
onChange={(e) => updateField(group.id, fieldIndex, { label: e.target.value })}
|
|
placeholder="라벨"
|
|
className="h-6 flex-1 text-xs"
|
|
/>
|
|
<Select
|
|
value={field.format || "text"}
|
|
onValueChange={(value) =>
|
|
updateField(group.id, fieldIndex, {
|
|
format: value as FieldConfig["format"],
|
|
})
|
|
}
|
|
>
|
|
<SelectTrigger className="h-6 w-20 text-xs">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="text">텍스트</SelectItem>
|
|
<SelectItem value="number">숫자</SelectItem>
|
|
<SelectItem value="date">날짜</SelectItem>
|
|
<SelectItem value="datetime">날짜시간</SelectItem>
|
|
<SelectItem value="currency">통화</SelectItem>
|
|
<SelectItem value="distance">거리</SelectItem>
|
|
<SelectItem value="duration">시간</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => removeField(group.id, fieldIndex)}
|
|
className="h-6 w-6 p-0 text-destructive"
|
|
>
|
|
<Trash2 className="h-3 w-3" />
|
|
</Button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|