ERP-node/frontend/lib/registry/components/conditional-container/ConditionalContainerConfigP...

690 lines
27 KiB
TypeScript
Raw Normal View History

"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 { Switch } from "@/components/ui/switch";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
2025-11-28 14:56:11 +09:00
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command";
import { Plus, Trash2, GripVertical, Loader2, Check, ChevronsUpDown, Database } from "lucide-react";
import { ConditionalContainerConfig, ConditionalSection } from "./types";
import { screenApi } from "@/lib/api/screen";
2025-11-28 14:56:11 +09:00
import { cn } from "@/lib/utils";
import { getCategoryColumnsByMenu, getCategoryValues, getSecondLevelMenus } from "@/lib/api/tableCategoryValue";
interface ConditionalContainerConfigPanelProps {
config: ConditionalContainerConfig;
2025-11-28 14:56:11 +09:00
onChange?: (config: ConditionalContainerConfig) => void;
onConfigChange?: (config: ConditionalContainerConfig) => void;
}
export function ConditionalContainerConfigPanel({
config,
2025-11-28 14:56:11 +09:00
onChange,
onConfigChange,
}: ConditionalContainerConfigPanelProps) {
2025-11-28 14:56:11 +09:00
// onChange 또는 onConfigChange 둘 다 지원
const handleConfigChange = onChange || onConfigChange;
const [localConfig, setLocalConfig] = useState<ConditionalContainerConfig>({
controlField: config.controlField || "condition",
controlLabel: config.controlLabel || "조건 선택",
sections: config.sections || [],
defaultValue: config.defaultValue || "",
showBorder: config.showBorder ?? true,
spacing: config.spacing || "normal",
});
// 화면 목록 상태
const [screens, setScreens] = useState<any[]>([]);
const [screensLoading, setScreensLoading] = useState(false);
2025-11-28 14:56:11 +09:00
// 🆕 메뉴 기반 카테고리 관련 상태
const [availableMenus, setAvailableMenus] = useState<Array<{ menuObjid: number; menuName: string; parentMenuName: string; screenCode?: string }>>([]);
const [menusLoading, setMenusLoading] = useState(false);
const [selectedMenuObjid, setSelectedMenuObjid] = useState<number | null>(null);
const [menuPopoverOpen, setMenuPopoverOpen] = useState(false);
const [categoryColumns, setCategoryColumns] = useState<Array<{ columnName: string; columnLabel: string; tableName: string }>>([]);
const [categoryColumnsLoading, setCategoryColumnsLoading] = useState(false);
const [selectedCategoryColumn, setSelectedCategoryColumn] = useState<string>("");
const [selectedCategoryTableName, setSelectedCategoryTableName] = useState<string>("");
const [columnPopoverOpen, setColumnPopoverOpen] = useState(false);
const [categoryValues, setCategoryValues] = useState<Array<{ value: string; label: string }>>([]);
const [categoryValuesLoading, setCategoryValuesLoading] = useState(false);
// 화면 목록 로드
useEffect(() => {
const loadScreens = async () => {
setScreensLoading(true);
try {
const response = await screenApi.getScreens({ page: 1, size: 1000 });
if (response.data) {
setScreens(response.data);
}
} catch (error) {
console.error("화면 목록 로드 실패:", error);
} finally {
setScreensLoading(false);
}
};
loadScreens();
}, []);
2025-11-28 14:56:11 +09:00
// 🆕 2레벨 메뉴 목록 로드
useEffect(() => {
const loadMenus = async () => {
setMenusLoading(true);
try {
const response = await getSecondLevelMenus();
console.log("🔍 [ConditionalContainer] 메뉴 목록 응답:", response);
if (response.success && response.data) {
setAvailableMenus(response.data);
}
} catch (error) {
console.error("메뉴 목록 로드 실패:", error);
} finally {
setMenusLoading(false);
}
};
loadMenus();
}, []);
// 🆕 선택된 메뉴의 카테고리 컬럼 목록 로드
useEffect(() => {
if (!selectedMenuObjid) {
setCategoryColumns([]);
setSelectedCategoryColumn("");
setSelectedCategoryTableName("");
setCategoryValues([]);
return;
}
const loadCategoryColumns = async () => {
setCategoryColumnsLoading(true);
try {
console.log("🔍 [ConditionalContainer] 메뉴별 카테고리 컬럼 로드:", selectedMenuObjid);
const response = await getCategoryColumnsByMenu(selectedMenuObjid);
console.log("✅ [ConditionalContainer] 카테고리 컬럼 응답:", response);
if (response.success && response.data) {
setCategoryColumns(response.data.map((col: any) => ({
columnName: col.columnName || col.column_name,
columnLabel: col.columnLabel || col.column_label || col.columnName || col.column_name,
tableName: col.tableName || col.table_name,
})));
} else {
setCategoryColumns([]);
}
} catch (error) {
console.error("카테고리 컬럼 로드 실패:", error);
setCategoryColumns([]);
} finally {
setCategoryColumnsLoading(false);
}
};
loadCategoryColumns();
}, [selectedMenuObjid]);
// 🆕 선택된 카테고리 컬럼의 값 목록 로드
useEffect(() => {
if (!selectedCategoryTableName || !selectedCategoryColumn || !selectedMenuObjid) {
setCategoryValues([]);
return;
}
const loadCategoryValues = async () => {
setCategoryValuesLoading(true);
try {
console.log("🔍 [ConditionalContainer] 카테고리 값 로드:", selectedCategoryTableName, selectedCategoryColumn, selectedMenuObjid);
const response = await getCategoryValues(selectedCategoryTableName, selectedCategoryColumn, false, selectedMenuObjid);
console.log("✅ [ConditionalContainer] 카테고리 값 응답:", response);
if (response.success && response.data) {
const values = response.data.map((v: any) => ({
value: v.valueCode || v.value_code,
label: v.valueLabel || v.value_label || v.valueCode || v.value_code,
}));
setCategoryValues(values);
} else {
setCategoryValues([]);
}
} catch (error) {
console.error("카테고리 값 로드 실패:", error);
setCategoryValues([]);
} finally {
setCategoryValuesLoading(false);
}
};
loadCategoryValues();
}, [selectedCategoryTableName, selectedCategoryColumn, selectedMenuObjid]);
// 🆕 테이블 카테고리에서 섹션 자동 생성
const generateSectionsFromCategory = () => {
if (categoryValues.length === 0) {
alert("먼저 테이블과 카테고리 컬럼을 선택하고 값을 로드해주세요.");
return;
}
const newSections: ConditionalSection[] = categoryValues.map((option, index) => ({
id: `section_${Date.now()}_${index}`,
condition: option.value,
label: option.label,
screenId: null,
screenName: undefined,
}));
updateConfig({
sections: newSections,
controlField: selectedCategoryColumn, // 카테고리 컬럼명을 제어 필드로 사용
});
alert(`${newSections.length}개의 섹션이 생성되었습니다.`);
};
// 설정 업데이트 헬퍼
const updateConfig = (updates: Partial<ConditionalContainerConfig>) => {
const newConfig = { ...localConfig, ...updates };
setLocalConfig(newConfig);
2025-11-28 14:56:11 +09:00
handleConfigChange?.(newConfig);
};
// 새 섹션 추가
const addSection = () => {
const newSection: ConditionalSection = {
id: `section_${Date.now()}`,
condition: `condition_${localConfig.sections.length + 1}`,
label: `조건 ${localConfig.sections.length + 1}`,
screenId: null,
screenName: undefined,
};
updateConfig({
sections: [...localConfig.sections, newSection],
});
};
// 섹션 삭제
const removeSection = (sectionId: string) => {
updateConfig({
sections: localConfig.sections.filter((s) => s.id !== sectionId),
});
};
// 섹션 업데이트
const updateSection = (
sectionId: string,
updates: Partial<ConditionalSection>
) => {
updateConfig({
sections: localConfig.sections.map((s) =>
s.id === sectionId ? { ...s, ...updates } : s
),
});
};
return (
<div className="space-y-6 p-4">
<div>
<h3 className="text-sm font-semibold mb-4"> </h3>
{/* 제어 필드 설정 */}
<div className="space-y-4 mb-6">
<div className="space-y-2">
<Label htmlFor="controlField" className="text-xs">
</Label>
<Input
id="controlField"
value={localConfig.controlField}
onChange={(e) => updateConfig({ controlField: e.target.value })}
placeholder="예: inputMode"
className="h-8 text-xs"
/>
<p className="text-[10px] text-muted-foreground">
formData에
</p>
</div>
<div className="space-y-2">
<Label htmlFor="controlLabel" className="text-xs">
</Label>
<Input
id="controlLabel"
value={localConfig.controlLabel}
onChange={(e) => updateConfig({ controlLabel: e.target.value })}
placeholder="예: 입력 방식"
className="h-8 text-xs"
/>
</div>
</div>
2025-11-28 14:56:11 +09:00
{/* 🆕 메뉴별 카테고리에서 섹션 자동 생성 */}
<div className="space-y-3 p-3 border rounded-lg bg-blue-50/50 dark:bg-blue-950/20">
<div className="flex items-center gap-2">
<Database className="h-4 w-4 text-blue-600" />
<Label className="text-xs font-semibold text-blue-700 dark:text-blue-400">
</Label>
</div>
{/* 1. 메뉴 선택 */}
<div className="space-y-2">
<Label className="text-[10px] text-muted-foreground">
1.
</Label>
<Popover open={menuPopoverOpen} onOpenChange={setMenuPopoverOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={menuPopoverOpen}
className="h-8 w-full justify-between text-xs"
disabled={menusLoading}
>
{menusLoading ? (
<>
<Loader2 className="mr-2 h-3 w-3 animate-spin" />
...
</>
) : selectedMenuObjid ? (
(() => {
const menu = availableMenus.find((m) => m.menuObjid === selectedMenuObjid);
return menu ? `${menu.parentMenuName} > ${menu.menuName}` : `메뉴 ${selectedMenuObjid}`;
})()
) : (
"메뉴 선택..."
)}
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[350px] p-0">
<Command>
<CommandInput placeholder="메뉴 검색..." className="h-8 text-xs" />
<CommandList>
<CommandEmpty className="py-2 text-xs"> </CommandEmpty>
<CommandGroup>
{availableMenus.map((menu) => (
<CommandItem
key={menu.menuObjid}
value={`${menu.parentMenuName} ${menu.menuName}`}
onSelect={() => {
setSelectedMenuObjid(menu.menuObjid);
setSelectedCategoryColumn("");
setSelectedCategoryTableName("");
setMenuPopoverOpen(false);
}}
className="text-xs"
>
<Check
className={cn(
"mr-2 h-3 w-3",
selectedMenuObjid === menu.menuObjid ? "opacity-100" : "opacity-0"
)}
/>
<div className="flex flex-col">
<span>{menu.parentMenuName} &gt; {menu.menuName}</span>
{menu.screenCode && (
<span className="text-[10px] text-muted-foreground">
{menu.screenCode}
</span>
)}
</div>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</div>
{/* 2. 카테고리 컬럼 선택 */}
{selectedMenuObjid && (
<div className="space-y-2">
<Label className="text-[10px] text-muted-foreground">
2.
</Label>
{categoryColumnsLoading ? (
<div className="flex items-center gap-2 text-xs text-muted-foreground h-8 px-3 border rounded">
<Loader2 className="h-3 w-3 animate-spin" />
...
</div>
) : categoryColumns.length > 0 ? (
<Popover open={columnPopoverOpen} onOpenChange={setColumnPopoverOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={columnPopoverOpen}
className="h-8 w-full justify-between text-xs"
>
{selectedCategoryColumn ? (
categoryColumns.find((c) => c.columnName === selectedCategoryColumn)?.columnLabel || selectedCategoryColumn
) : (
"카테고리 컬럼 선택..."
)}
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[280px] p-0">
<Command>
<CommandInput placeholder="컬럼 검색..." className="h-8 text-xs" />
<CommandList>
<CommandEmpty className="py-2 text-xs"> </CommandEmpty>
<CommandGroup>
{categoryColumns.map((col) => (
<CommandItem
key={`${col.tableName}.${col.columnName}`}
value={col.columnName}
onSelect={() => {
setSelectedCategoryColumn(col.columnName);
setSelectedCategoryTableName(col.tableName);
setColumnPopoverOpen(false);
}}
className="text-xs"
>
<Check
className={cn(
"mr-2 h-3 w-3",
selectedCategoryColumn === col.columnName ? "opacity-100" : "opacity-0"
)}
/>
<div className="flex flex-col">
<span>{col.columnLabel}</span>
<span className="text-[10px] text-muted-foreground">
{col.tableName}.{col.columnName}
</span>
</div>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
) : (
<p className="text-[10px] text-amber-600 dark:text-amber-400">
.
.
</p>
)}
</div>
)}
{/* 3. 카테고리 값 미리보기 */}
{selectedCategoryColumn && (
<div className="space-y-2">
<Label className="text-[10px] text-muted-foreground">
3.
</Label>
{categoryValuesLoading ? (
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<Loader2 className="h-3 w-3 animate-spin" />
...
</div>
) : categoryValues.length > 0 ? (
<div className="flex flex-wrap gap-1">
{categoryValues.map((option) => (
<span
key={option.value}
className="px-2 py-0.5 text-[10px] bg-blue-100 text-blue-800 rounded dark:bg-blue-900 dark:text-blue-200"
>
{option.label}
</span>
))}
</div>
) : (
<p className="text-[10px] text-amber-600 dark:text-amber-400">
.
.
</p>
)}
</div>
)}
<Button
onClick={generateSectionsFromCategory}
size="sm"
variant="default"
className="h-7 w-full text-xs"
disabled={!selectedCategoryColumn || categoryValues.length === 0 || categoryValuesLoading}
>
<Plus className="h-3 w-3 mr-1" />
{categoryValues.length > 0 ? `${categoryValues.length}개 섹션 자동 생성` : "섹션 자동 생성"}
</Button>
<p className="text-[10px] text-muted-foreground">
.
.
</p>
</div>
{/* 조건별 섹션 설정 */}
<div className="space-y-4">
<div className="flex items-center justify-between">
<Label className="text-xs font-semibold"> </Label>
<Button
onClick={addSection}
size="sm"
variant="outline"
className="h-7 text-xs"
>
<Plus className="h-3 w-3 mr-1" />
</Button>
</div>
{localConfig.sections.length === 0 ? (
<div className="text-center py-8 border-2 border-dashed rounded-lg">
<p className="text-xs text-muted-foreground">
</p>
</div>
) : (
<div className="space-y-3">
{localConfig.sections.map((section, index) => (
<div
key={section.id}
className="p-3 border rounded-lg space-y-3 bg-muted/20"
>
{/* 섹션 헤더 */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<GripVertical className="h-4 w-4 text-muted-foreground" />
<span className="text-xs font-medium">
{index + 1}
</span>
</div>
<Button
onClick={() => removeSection(section.id)}
size="sm"
variant="ghost"
className="h-6 w-6 p-0 text-destructive hover:text-destructive"
>
<Trash2 className="h-3 w-3" />
</Button>
</div>
{/* 조건 값 */}
<div className="space-y-1.5">
<Label className="text-[10px] text-muted-foreground">
()
</Label>
<Input
value={section.condition}
onChange={(e) =>
updateSection(section.id, { condition: e.target.value })
}
placeholder="예: customer_first"
className="h-7 text-xs"
/>
</div>
{/* 조건 라벨 */}
<div className="space-y-1.5">
<Label className="text-[10px] text-muted-foreground">
</Label>
<Input
value={section.label}
onChange={(e) =>
updateSection(section.id, { label: e.target.value })
}
placeholder="예: 거래처 우선"
className="h-7 text-xs"
/>
</div>
{/* 화면 선택 */}
<div className="space-y-1">
<Label className="text-[10px] text-muted-foreground">
</Label>
{screensLoading ? (
<div className="flex items-center gap-2 text-xs text-muted-foreground h-7 px-3 border rounded">
<Loader2 className="h-3 w-3 animate-spin" />
...
</div>
) : (
<Select
value={section.screenId?.toString() || "none"}
onValueChange={(value) => {
if (value === "none") {
updateSection(section.id, {
screenId: null,
screenName: undefined,
});
} else {
const screenId = parseInt(value);
const selectedScreen = screens.find(
(s) => s.screenId === screenId
);
updateSection(section.id, {
screenId,
screenName: selectedScreen?.screenName,
});
}
}}
>
<SelectTrigger className="h-7 text-xs">
<SelectValue placeholder="화면 선택..." />
</SelectTrigger>
<SelectContent>
<SelectItem value="none"> </SelectItem>
{screens.map((screen) => (
<SelectItem
key={screen.screenId}
value={screen.screenId.toString()}
>
{screen.screenName}
{screen.description && (
<span className="text-[10px] text-muted-foreground ml-1">
({screen.description})
</span>
)}
</SelectItem>
))}
</SelectContent>
</Select>
)}
{section.screenId && (
<div className="text-[10px] text-muted-foreground">
ID: {section.screenId}
</div>
)}
</div>
</div>
))}
</div>
)}
</div>
{/* 기본값 설정 */}
{localConfig.sections.length > 0 && (
<div className="space-y-2 mt-4">
<Label htmlFor="defaultValue" className="text-xs">
</Label>
<Select
value={localConfig.defaultValue || ""}
onValueChange={(value) => updateConfig({ defaultValue: value })}
>
<SelectTrigger className="h-8 text-xs">
<SelectValue placeholder="기본값 선택" />
</SelectTrigger>
<SelectContent>
{localConfig.sections.map((section) => (
<SelectItem key={section.id} value={section.condition}>
{section.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}
{/* 스타일 설정 */}
<div className="space-y-4 mt-6 pt-6 border-t">
<Label className="text-xs font-semibold"> </Label>
{/* 테두리 표시 */}
<div className="flex items-center justify-between">
<Label htmlFor="showBorder" className="text-xs">
</Label>
<Switch
id="showBorder"
checked={localConfig.showBorder}
onCheckedChange={(checked) =>
updateConfig({ showBorder: checked })
}
/>
</div>
{/* 간격 설정 */}
<div className="space-y-2">
<Label htmlFor="spacing" className="text-xs">
</Label>
<Select
value={localConfig.spacing || "normal"}
onValueChange={(value: any) => updateConfig({ spacing: value })}
>
<SelectTrigger className="h-8 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="tight"></SelectItem>
<SelectItem value="normal"></SelectItem>
<SelectItem value="loose"></SelectItem>
</SelectContent>
</Select>
</div>
</div>
</div>
</div>
);
}