551 lines
24 KiB
TypeScript
551 lines
24 KiB
TypeScript
"use client";
|
|
|
|
import React, { useState, useEffect } from "react";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Checkbox } from "@/components/ui/checkbox";
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Separator } from "@/components/ui/separator";
|
|
import { Settings, Layout, ArrowRight, Database, Loader2, Check, ChevronsUpDown, Plus, Trash2, Link2 } from "lucide-react";
|
|
import { screenApi } from "@/lib/api/screen";
|
|
import { getTableColumns } from "@/lib/api/tableManagement";
|
|
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
|
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
|
import { cn } from "@/lib/utils";
|
|
import type { ParentDataMapping } from "@/contexts/SplitPanelContext";
|
|
|
|
interface ScreenSplitPanelConfigPanelProps {
|
|
config: any;
|
|
onChange: (newConfig: any) => void;
|
|
}
|
|
|
|
export function ScreenSplitPanelConfigPanel({ config = {}, onChange }: ScreenSplitPanelConfigPanelProps) {
|
|
// 화면 목록 상태
|
|
const [screens, setScreens] = useState<any[]>([]);
|
|
const [isLoadingScreens, setIsLoadingScreens] = useState(true);
|
|
|
|
// Combobox 상태
|
|
const [leftOpen, setLeftOpen] = useState(false);
|
|
const [rightOpen, setRightOpen] = useState(false);
|
|
|
|
// 좌측 화면의 테이블 컬럼 목록
|
|
const [leftScreenColumns, setLeftScreenColumns] = useState<Array<{ columnName: string; columnLabel: string }>>([]);
|
|
const [isLoadingColumns, setIsLoadingColumns] = useState(false);
|
|
|
|
const [localConfig, setLocalConfig] = useState({
|
|
screenId: config.screenId || 0,
|
|
leftScreenId: config.leftScreenId || 0,
|
|
rightScreenId: config.rightScreenId || 0,
|
|
splitRatio: config.splitRatio || 50,
|
|
resizable: config.resizable ?? true,
|
|
buttonLabel: config.buttonLabel || "데이터 전달",
|
|
buttonPosition: config.buttonPosition || "center",
|
|
parentDataMapping: config.parentDataMapping || [] as ParentDataMapping[],
|
|
...config,
|
|
});
|
|
|
|
// config prop이 변경되면 localConfig 동기화
|
|
useEffect(() => {
|
|
console.log("🔄 [ScreenSplitPanelConfigPanel] config prop 변경 감지:", config);
|
|
setLocalConfig({
|
|
screenId: config.screenId || 0,
|
|
leftScreenId: config.leftScreenId || 0,
|
|
rightScreenId: config.rightScreenId || 0,
|
|
splitRatio: config.splitRatio || 50,
|
|
resizable: config.resizable ?? true,
|
|
buttonLabel: config.buttonLabel || "데이터 전달",
|
|
buttonPosition: config.buttonPosition || "center",
|
|
parentDataMapping: config.parentDataMapping || [],
|
|
...config,
|
|
});
|
|
}, [config]);
|
|
|
|
// 좌측 화면이 변경되면 해당 화면의 테이블 컬럼 로드
|
|
useEffect(() => {
|
|
const loadLeftScreenColumns = async () => {
|
|
if (!localConfig.leftScreenId) {
|
|
setLeftScreenColumns([]);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
setIsLoadingColumns(true);
|
|
|
|
// 좌측 화면 정보 조회
|
|
const screenData = await screenApi.getScreen(localConfig.leftScreenId);
|
|
if (!screenData?.tableName) {
|
|
console.warn("좌측 화면에 테이블이 설정되지 않았습니다.");
|
|
setLeftScreenColumns([]);
|
|
return;
|
|
}
|
|
|
|
// 테이블 컬럼 조회
|
|
const columnsResponse = await getTableColumns(screenData.tableName);
|
|
if (columnsResponse.success && columnsResponse.data?.columns) {
|
|
const columns = columnsResponse.data.columns.map((col: any) => ({
|
|
columnName: col.column_name || col.columnName,
|
|
columnLabel: col.column_label || col.columnLabel || col.column_name || col.columnName,
|
|
}));
|
|
setLeftScreenColumns(columns);
|
|
console.log("📋 좌측 화면 컬럼 로드 완료:", columns.length);
|
|
}
|
|
} catch (error) {
|
|
console.error("좌측 화면 컬럼 로드 실패:", error);
|
|
setLeftScreenColumns([]);
|
|
} finally {
|
|
setIsLoadingColumns(false);
|
|
}
|
|
};
|
|
|
|
loadLeftScreenColumns();
|
|
}, [localConfig.leftScreenId]);
|
|
|
|
// 화면 목록 로드
|
|
useEffect(() => {
|
|
const loadScreens = async () => {
|
|
try {
|
|
setIsLoadingScreens(true);
|
|
const response = await screenApi.getScreens({ page: 1, size: 1000 });
|
|
if (response.data) {
|
|
setScreens(response.data);
|
|
}
|
|
} catch (error) {
|
|
console.error("화면 목록 로드 실패:", error);
|
|
} finally {
|
|
setIsLoadingScreens(false);
|
|
}
|
|
};
|
|
|
|
loadScreens();
|
|
}, []);
|
|
|
|
const updateConfig = (key: string, value: any) => {
|
|
const newConfig = {
|
|
...localConfig,
|
|
[key]: value,
|
|
};
|
|
setLocalConfig(newConfig);
|
|
|
|
console.log("📝 [ScreenSplitPanelConfigPanel] 설정 변경:", {
|
|
key,
|
|
value,
|
|
newConfig,
|
|
hasOnChange: !!onChange,
|
|
});
|
|
|
|
// 변경 즉시 부모에게 전달
|
|
if (onChange) {
|
|
onChange(newConfig);
|
|
}
|
|
};
|
|
|
|
// 부모 데이터 매핑 추가
|
|
const addParentDataMapping = () => {
|
|
const newMapping: ParentDataMapping = {
|
|
sourceColumn: "",
|
|
targetColumn: "",
|
|
};
|
|
const newMappings = [...(localConfig.parentDataMapping || []), newMapping];
|
|
updateConfig("parentDataMapping", newMappings);
|
|
};
|
|
|
|
// 부모 데이터 매핑 수정
|
|
const updateParentDataMapping = (index: number, field: keyof ParentDataMapping, value: string) => {
|
|
const newMappings = [...(localConfig.parentDataMapping || [])];
|
|
newMappings[index] = {
|
|
...newMappings[index],
|
|
[field]: value,
|
|
};
|
|
updateConfig("parentDataMapping", newMappings);
|
|
};
|
|
|
|
// 부모 데이터 매핑 삭제
|
|
const removeParentDataMapping = (index: number) => {
|
|
const newMappings = (localConfig.parentDataMapping || []).filter((_: any, i: number) => i !== index);
|
|
updateConfig("parentDataMapping", newMappings);
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<Tabs defaultValue="layout" className="w-full">
|
|
<TabsList className="grid w-full grid-cols-3">
|
|
<TabsTrigger value="layout" className="gap-2">
|
|
<Layout className="h-4 w-4" />
|
|
레이아웃
|
|
</TabsTrigger>
|
|
<TabsTrigger value="screens" className="gap-2">
|
|
<Database className="h-4 w-4" />
|
|
화면 설정
|
|
</TabsTrigger>
|
|
<TabsTrigger value="dataMapping" className="gap-2">
|
|
<Link2 className="h-4 w-4" />
|
|
데이터 전달
|
|
</TabsTrigger>
|
|
</TabsList>
|
|
|
|
{/* 레이아웃 탭 */}
|
|
<TabsContent value="layout" className="space-y-4">
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="text-base">분할 비율</CardTitle>
|
|
<CardDescription className="text-xs">좌측과 우측 패널의 너비 비율을 설정합니다</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
<div className="space-y-2">
|
|
<div className="flex items-center justify-between">
|
|
<Label htmlFor="splitRatio" className="text-xs">
|
|
좌측 패널 너비 (%)
|
|
</Label>
|
|
<span className="text-xs font-medium">{localConfig.splitRatio}%</span>
|
|
</div>
|
|
<Input
|
|
id="splitRatio"
|
|
type="range"
|
|
min="20"
|
|
max="80"
|
|
step="5"
|
|
value={localConfig.splitRatio}
|
|
onChange={(e) => updateConfig("splitRatio", parseInt(e.target.value))}
|
|
className="h-2"
|
|
/>
|
|
<div className="text-muted-foreground flex justify-between text-xs">
|
|
<span>20%</span>
|
|
<span>50%</span>
|
|
<span>80%</span>
|
|
</div>
|
|
</div>
|
|
|
|
<Separator />
|
|
|
|
<div className="flex items-center justify-between">
|
|
<div className="space-y-0.5">
|
|
<Label htmlFor="resizable" className="text-xs font-medium">
|
|
크기 조절 가능
|
|
</Label>
|
|
<p className="text-muted-foreground text-xs">사용자가 패널 크기를 조절할 수 있습니다</p>
|
|
</div>
|
|
<Checkbox
|
|
id="resizable"
|
|
checked={localConfig.resizable}
|
|
onCheckedChange={(checked) => updateConfig("resizable", checked)}
|
|
/>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</TabsContent>
|
|
|
|
{/* 화면 설정 탭 */}
|
|
<TabsContent value="screens" className="space-y-4">
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="text-base">임베드할 화면 선택</CardTitle>
|
|
<CardDescription className="text-xs">좌측과 우측에 표시할 화면을 선택합니다</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
{isLoadingScreens ? (
|
|
<div className="flex items-center justify-center py-8">
|
|
<Loader2 className="text-muted-foreground h-6 w-6 animate-spin" />
|
|
<span className="text-muted-foreground ml-2 text-xs">화면 목록 로딩 중...</span>
|
|
</div>
|
|
) : (
|
|
<>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="leftScreenId" className="text-xs">
|
|
좌측 화면 (소스)
|
|
</Label>
|
|
<Popover open={leftOpen} onOpenChange={setLeftOpen}>
|
|
<PopoverTrigger asChild>
|
|
<Button
|
|
variant="outline"
|
|
role="combobox"
|
|
aria-expanded={leftOpen}
|
|
className="h-9 w-full justify-between text-xs"
|
|
>
|
|
{localConfig.leftScreenId
|
|
? screens.find((s) => s.screenId === localConfig.leftScreenId)?.screenName || "화면 선택..."
|
|
: "화면 선택..."}
|
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent
|
|
className="p-0"
|
|
style={{ width: "var(--radix-popover-trigger-width)" }}
|
|
align="start"
|
|
>
|
|
<Command>
|
|
<CommandInput placeholder="화면 검색..." className="text-xs" />
|
|
<CommandList>
|
|
<CommandEmpty className="text-xs">화면을 찾을 수 없습니다.</CommandEmpty>
|
|
<CommandGroup>
|
|
{screens.map((screen) => (
|
|
<CommandItem
|
|
key={screen.screenId}
|
|
value={`${screen.screenName} ${screen.screenCode}`}
|
|
onSelect={() => {
|
|
updateConfig("leftScreenId", screen.screenId);
|
|
setLeftOpen(false);
|
|
}}
|
|
className="text-xs"
|
|
>
|
|
<Check
|
|
className={cn(
|
|
"mr-2 h-4 w-4",
|
|
localConfig.leftScreenId === screen.screenId ? "opacity-100" : "opacity-0",
|
|
)}
|
|
/>
|
|
<div className="flex flex-col">
|
|
<span className="font-medium">{screen.screenName}</span>
|
|
<span className="text-[10px] text-gray-500">{screen.screenCode}</span>
|
|
</div>
|
|
</CommandItem>
|
|
))}
|
|
</CommandGroup>
|
|
</CommandList>
|
|
</Command>
|
|
</PopoverContent>
|
|
</Popover>
|
|
<p className="text-muted-foreground text-xs">데이터를 선택할 소스 화면</p>
|
|
</div>
|
|
|
|
<Separator />
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="rightScreenId" className="text-xs">
|
|
우측 화면 (타겟)
|
|
</Label>
|
|
<Popover open={rightOpen} onOpenChange={setRightOpen}>
|
|
<PopoverTrigger asChild>
|
|
<Button
|
|
variant="outline"
|
|
role="combobox"
|
|
aria-expanded={rightOpen}
|
|
className="h-9 w-full justify-between text-xs"
|
|
>
|
|
{localConfig.rightScreenId
|
|
? screens.find((s) => s.screenId === localConfig.rightScreenId)?.screenName ||
|
|
"화면 선택..."
|
|
: "화면 선택..."}
|
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent
|
|
className="p-0"
|
|
style={{ width: "var(--radix-popover-trigger-width)" }}
|
|
align="start"
|
|
>
|
|
<Command>
|
|
<CommandInput placeholder="화면 검색..." className="text-xs" />
|
|
<CommandList>
|
|
<CommandEmpty className="text-xs">화면을 찾을 수 없습니다.</CommandEmpty>
|
|
<CommandGroup>
|
|
{screens.map((screen) => (
|
|
<CommandItem
|
|
key={screen.screenId}
|
|
value={`${screen.screenName} ${screen.screenCode}`}
|
|
onSelect={() => {
|
|
updateConfig("rightScreenId", screen.screenId);
|
|
setRightOpen(false);
|
|
}}
|
|
className="text-xs"
|
|
>
|
|
<Check
|
|
className={cn(
|
|
"mr-2 h-4 w-4",
|
|
localConfig.rightScreenId === screen.screenId ? "opacity-100" : "opacity-0",
|
|
)}
|
|
/>
|
|
<div className="flex flex-col">
|
|
<span className="font-medium">{screen.screenName}</span>
|
|
<span className="text-[10px] text-gray-500">{screen.screenCode}</span>
|
|
</div>
|
|
</CommandItem>
|
|
))}
|
|
</CommandGroup>
|
|
</CommandList>
|
|
</Command>
|
|
</PopoverContent>
|
|
</Popover>
|
|
<p className="text-muted-foreground text-xs">데이터를 받을 타겟 화면</p>
|
|
</div>
|
|
|
|
<div className="rounded-lg border border-amber-200 bg-amber-50 p-3 dark:border-amber-900 dark:bg-amber-950">
|
|
<p className="text-xs text-amber-800 dark:text-amber-200">
|
|
데이터 전달 방법: 좌측 화면에 테이블과 버튼을 배치하고, 버튼의 액션을
|
|
"transferData"로 설정하세요.
|
|
<br />
|
|
버튼 설정에서 소스 컴포넌트(테이블), 타겟 화면, 필드 매핑을 지정할 수 있습니다.
|
|
</p>
|
|
</div>
|
|
</>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</TabsContent>
|
|
|
|
{/* 데이터 전달 탭 */}
|
|
<TabsContent value="dataMapping" className="space-y-4">
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="text-base">부모 데이터 자동 전달</CardTitle>
|
|
<CardDescription className="text-xs">
|
|
좌측 화면에서 행을 선택하면, 우측 화면의 추가/저장 시 지정된 컬럼 값이 자동으로 포함됩니다.
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
{!localConfig.leftScreenId ? (
|
|
<div className="rounded-lg border border-amber-200 bg-amber-50 p-3 dark:border-amber-900 dark:bg-amber-950">
|
|
<p className="text-xs text-amber-800 dark:text-amber-200">
|
|
먼저 "화면 설정" 탭에서 좌측 화면을 선택하세요.
|
|
</p>
|
|
</div>
|
|
) : isLoadingColumns ? (
|
|
<div className="flex items-center justify-center py-4">
|
|
<Loader2 className="text-muted-foreground h-5 w-5 animate-spin" />
|
|
<span className="text-muted-foreground ml-2 text-xs">컬럼 정보 로딩 중...</span>
|
|
</div>
|
|
) : leftScreenColumns.length === 0 ? (
|
|
<div className="rounded-lg border border-amber-200 bg-amber-50 p-3 dark:border-amber-900 dark:bg-amber-950">
|
|
<p className="text-xs text-amber-800 dark:text-amber-200">
|
|
좌측 화면에 테이블이 설정되지 않았거나 컬럼 정보를 불러올 수 없습니다.
|
|
</p>
|
|
</div>
|
|
) : (
|
|
<>
|
|
{/* 매핑 목록 */}
|
|
<div className="space-y-3">
|
|
{(localConfig.parentDataMapping || []).map((mapping: ParentDataMapping, index: number) => (
|
|
<div key={index} className="flex items-center gap-2 rounded-lg border bg-gray-50 p-3 dark:bg-gray-900">
|
|
<div className="flex-1 space-y-2">
|
|
<div className="flex items-center gap-2">
|
|
<div className="flex-1">
|
|
<Label className="text-xs text-gray-600">소스 컬럼 (좌측)</Label>
|
|
<Select
|
|
value={mapping.sourceColumn}
|
|
onValueChange={(value) => updateParentDataMapping(index, "sourceColumn", value)}
|
|
>
|
|
<SelectTrigger className="h-8 text-xs">
|
|
<SelectValue placeholder="컬럼 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{leftScreenColumns.map((col) => (
|
|
<SelectItem key={col.columnName} value={col.columnName} className="text-xs">
|
|
{col.columnLabel} ({col.columnName})
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<ArrowRight className="mt-5 h-4 w-4 text-gray-400" />
|
|
<div className="flex-1">
|
|
<Label className="text-xs text-gray-600">타겟 컬럼 (우측 저장 시)</Label>
|
|
<Input
|
|
value={mapping.targetColumn}
|
|
onChange={(e) => updateParentDataMapping(index, "targetColumn", e.target.value)}
|
|
placeholder="저장할 컬럼명"
|
|
className="h-8 text-xs"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => removeParentDataMapping(index)}
|
|
className="h-8 w-8 p-0 text-red-500 hover:bg-red-50 hover:text-red-600"
|
|
>
|
|
<Trash2 className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{/* 매핑 추가 버튼 */}
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={addParentDataMapping}
|
|
className="w-full"
|
|
>
|
|
<Plus className="mr-2 h-4 w-4" />
|
|
매핑 추가
|
|
</Button>
|
|
|
|
{/* 안내 메시지 */}
|
|
<div className="rounded-lg border border-blue-200 bg-blue-50 p-3 dark:border-blue-900 dark:bg-blue-950">
|
|
<p className="text-xs text-blue-800 dark:text-blue-200">
|
|
<strong>사용 예시:</strong>
|
|
<br />
|
|
좌측: 설비 목록 (equipment_mng)
|
|
<br />
|
|
우측: 점검항목 추가 화면
|
|
<br />
|
|
<br />
|
|
매핑 설정:
|
|
<br />
|
|
- 소스: equipment_code → 타겟: equipment_code
|
|
<br />
|
|
<br />
|
|
좌측에서 설비를 선택하고 우측에서 점검항목을 추가하면,
|
|
선택한 설비의 equipment_code가 자동으로 저장됩니다.
|
|
</p>
|
|
</div>
|
|
</>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</TabsContent>
|
|
</Tabs>
|
|
|
|
{/* 설정 요약 */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="text-sm">현재 설정</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="space-y-1 text-xs">
|
|
<div className="flex justify-between">
|
|
<span className="text-muted-foreground">좌측 화면:</span>
|
|
<span className="font-medium">
|
|
{localConfig.leftScreenId
|
|
? screens.find((s) => s.screenId === localConfig.leftScreenId)?.screenName ||
|
|
`ID: ${localConfig.leftScreenId}`
|
|
: "미설정"}
|
|
</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span className="text-muted-foreground">우측 화면:</span>
|
|
<span className="font-medium">
|
|
{localConfig.rightScreenId
|
|
? screens.find((s) => s.screenId === localConfig.rightScreenId)?.screenName ||
|
|
`ID: ${localConfig.rightScreenId}`
|
|
: "미설정"}
|
|
</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span className="text-muted-foreground">분할 비율:</span>
|
|
<span className="font-medium">
|
|
{localConfig.splitRatio}% / {100 - localConfig.splitRatio}%
|
|
</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span className="text-muted-foreground">크기 조절:</span>
|
|
<span className="font-medium">{localConfig.resizable ? "가능" : "불가능"}</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span className="text-muted-foreground">데이터 매핑:</span>
|
|
<span className="font-medium">
|
|
{(localConfig.parentDataMapping || []).length > 0
|
|
? `${localConfig.parentDataMapping.length}개 설정`
|
|
: "미설정"}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|