- Enhanced the `ScreenManagementService` to include updates for V2 layouts in the `screen_layouts_v2` table. - Implemented logic to remap `screenId`, `targetScreenId`, `modalScreenId`, and other related IDs in layout data. - Added logging for the number of layouts updated in both V1 and V2, improving traceability of the update process. - This update ensures that screen references are correctly maintained across different layout versions, enhancing the overall functionality of the screen management system.
265 lines
7.7 KiB
TypeScript
265 lines
7.7 KiB
TypeScript
"use client";
|
|
|
|
import React from "react";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Switch } from "@/components/ui/switch";
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/components/ui/select";
|
|
import { Textarea } from "@/components/ui/textarea";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Plus, X } from "lucide-react";
|
|
import { ConfigFieldDefinition, ConfigOption } from "./ConfigPanelTypes";
|
|
|
|
interface ConfigFieldProps<T = any> {
|
|
field: ConfigFieldDefinition<T>;
|
|
value: any;
|
|
onChange: (key: string, value: any) => void;
|
|
tableColumns?: ConfigOption[];
|
|
}
|
|
|
|
export function ConfigField<T>({
|
|
field,
|
|
value,
|
|
onChange,
|
|
tableColumns,
|
|
}: ConfigFieldProps<T>) {
|
|
const handleChange = (newValue: any) => {
|
|
onChange(field.key, newValue);
|
|
};
|
|
|
|
const renderField = () => {
|
|
switch (field.type) {
|
|
case "text":
|
|
return (
|
|
<Input
|
|
value={value ?? ""}
|
|
onChange={(e) => handleChange(e.target.value)}
|
|
placeholder={field.placeholder}
|
|
className="h-8 text-xs"
|
|
/>
|
|
);
|
|
|
|
case "number":
|
|
return (
|
|
<Input
|
|
type="number"
|
|
value={value ?? ""}
|
|
onChange={(e) =>
|
|
handleChange(
|
|
e.target.value === "" ? undefined : Number(e.target.value),
|
|
)
|
|
}
|
|
placeholder={field.placeholder}
|
|
min={field.min}
|
|
max={field.max}
|
|
step={field.step}
|
|
className="h-8 text-xs"
|
|
/>
|
|
);
|
|
|
|
case "switch":
|
|
return (
|
|
<Switch
|
|
checked={!!value}
|
|
onCheckedChange={handleChange}
|
|
/>
|
|
);
|
|
|
|
case "select":
|
|
return (
|
|
<Select
|
|
value={value ?? ""}
|
|
onValueChange={handleChange}
|
|
>
|
|
<SelectTrigger className="h-8 text-xs">
|
|
<SelectValue placeholder={field.placeholder || "선택"} />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{(field.options || []).map((opt) => (
|
|
<SelectItem key={opt.value} value={opt.value} className="text-xs">
|
|
{opt.label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
);
|
|
|
|
case "textarea":
|
|
return (
|
|
<Textarea
|
|
value={value ?? ""}
|
|
onChange={(e) => handleChange(e.target.value)}
|
|
placeholder={field.placeholder}
|
|
className="text-xs"
|
|
rows={3}
|
|
/>
|
|
);
|
|
|
|
case "color":
|
|
return (
|
|
<div className="flex items-center gap-2">
|
|
<input
|
|
type="color"
|
|
value={value ?? "#000000"}
|
|
onChange={(e) => handleChange(e.target.value)}
|
|
className="h-8 w-8 cursor-pointer rounded border"
|
|
/>
|
|
<Input
|
|
value={value ?? ""}
|
|
onChange={(e) => handleChange(e.target.value)}
|
|
placeholder="#000000"
|
|
className="h-8 flex-1 text-xs"
|
|
/>
|
|
</div>
|
|
);
|
|
|
|
case "slider":
|
|
return (
|
|
<div className="flex items-center gap-2">
|
|
<Input
|
|
type="number"
|
|
value={value ?? field.min ?? 0}
|
|
onChange={(e) => handleChange(Number(e.target.value))}
|
|
min={field.min}
|
|
max={field.max}
|
|
step={field.step}
|
|
className="h-8 w-20 text-xs"
|
|
/>
|
|
<span className="text-muted-foreground text-[10px]">
|
|
{field.min ?? 0} ~ {field.max ?? 100}
|
|
</span>
|
|
</div>
|
|
);
|
|
|
|
case "multi-select":
|
|
return (
|
|
<div className="space-y-1">
|
|
{(field.options || []).map((opt) => {
|
|
const selected = Array.isArray(value) && value.includes(opt.value);
|
|
return (
|
|
<label
|
|
key={opt.value}
|
|
className="flex cursor-pointer items-center gap-2 rounded px-1 py-0.5 hover:bg-muted"
|
|
>
|
|
<input
|
|
type="checkbox"
|
|
checked={selected}
|
|
onChange={() => {
|
|
const current = Array.isArray(value) ? [...value] : [];
|
|
if (selected) {
|
|
handleChange(current.filter((v: string) => v !== opt.value));
|
|
} else {
|
|
handleChange([...current, opt.value]);
|
|
}
|
|
}}
|
|
className="rounded"
|
|
/>
|
|
<span className="text-xs">{opt.label}</span>
|
|
</label>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
|
|
case "key-value": {
|
|
const entries: Array<[string, string]> = Object.entries(
|
|
(value as Record<string, string>) || {},
|
|
);
|
|
return (
|
|
<div className="space-y-1">
|
|
{entries.map(([k, v], idx) => (
|
|
<div key={idx} className="flex items-center gap-1">
|
|
<Input
|
|
value={k}
|
|
onChange={(e) => {
|
|
const newObj = { ...(value || {}) };
|
|
delete newObj[k];
|
|
newObj[e.target.value] = v;
|
|
handleChange(newObj);
|
|
}}
|
|
placeholder="키"
|
|
className="h-7 flex-1 text-xs"
|
|
/>
|
|
<Input
|
|
value={v}
|
|
onChange={(e) => {
|
|
handleChange({ ...(value || {}), [k]: e.target.value });
|
|
}}
|
|
placeholder="값"
|
|
className="h-7 flex-1 text-xs"
|
|
/>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-7 w-7"
|
|
onClick={() => {
|
|
const newObj = { ...(value || {}) };
|
|
delete newObj[k];
|
|
handleChange(newObj);
|
|
}}
|
|
>
|
|
<X className="h-3 w-3" />
|
|
</Button>
|
|
</div>
|
|
))}
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className="h-7 w-full text-xs"
|
|
onClick={() => {
|
|
handleChange({ ...(value || {}), "": "" });
|
|
}}
|
|
>
|
|
<Plus className="mr-1 h-3 w-3" />
|
|
추가
|
|
</Button>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
case "column-picker": {
|
|
const options = tableColumns || field.options || [];
|
|
return (
|
|
<Select
|
|
value={value ?? ""}
|
|
onValueChange={handleChange}
|
|
>
|
|
<SelectTrigger className="h-8 text-xs">
|
|
<SelectValue placeholder={field.placeholder || "컬럼 선택"} />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{options.map((opt) => (
|
|
<SelectItem key={opt.value} value={opt.value} className="text-xs">
|
|
{opt.label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
);
|
|
}
|
|
|
|
default:
|
|
return null;
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-1">
|
|
<div className="flex items-center justify-between">
|
|
<Label className="text-xs font-medium">{field.label}</Label>
|
|
{field.type === "switch" && renderField()}
|
|
</div>
|
|
{field.description && (
|
|
<p className="text-muted-foreground text-[10px]">{field.description}</p>
|
|
)}
|
|
{field.type !== "switch" && renderField()}
|
|
</div>
|
|
);
|
|
}
|