2025-11-14 17:40:07 +09:00
|
|
|
"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";
|
2025-11-14 17:40:07 +09:00
|
|
|
import { ConditionalContainerConfig, ConditionalSection } from "./types";
|
2025-11-14 18:00:56 +09:00
|
|
|
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";
|
2025-11-14 17:40:07 +09:00
|
|
|
|
|
|
|
|
interface ConditionalContainerConfigPanelProps {
|
|
|
|
|
config: ConditionalContainerConfig;
|
2025-11-28 14:56:11 +09:00
|
|
|
onChange?: (config: ConditionalContainerConfig) => void;
|
|
|
|
|
onConfigChange?: (config: ConditionalContainerConfig) => void;
|
2025-11-14 17:40:07 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function ConditionalContainerConfigPanel({
|
|
|
|
|
config,
|
2025-11-28 14:56:11 +09:00
|
|
|
onChange,
|
2025-11-14 17:40:07 +09:00
|
|
|
onConfigChange,
|
|
|
|
|
}: ConditionalContainerConfigPanelProps) {
|
2025-11-28 14:56:11 +09:00
|
|
|
// onChange 또는 onConfigChange 둘 다 지원
|
|
|
|
|
const handleConfigChange = onChange || onConfigChange;
|
2025-11-14 17:40:07 +09:00
|
|
|
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);
|
|
|
|
|
|
2025-11-14 17:40:07 +09:00
|
|
|
// 화면 목록 로드
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const loadScreens = async () => {
|
|
|
|
|
setScreensLoading(true);
|
|
|
|
|
try {
|
2025-11-14 18:00:56 +09:00
|
|
|
const response = await screenApi.getScreens({ page: 1, size: 1000 });
|
|
|
|
|
if (response.data) {
|
2025-11-14 17:40:07 +09:00
|
|
|
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}개의 섹션이 생성되었습니다.`);
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-14 17:40:07 +09:00
|
|
|
// 설정 업데이트 헬퍼
|
|
|
|
|
const updateConfig = (updates: Partial<ConditionalContainerConfig>) => {
|
|
|
|
|
const newConfig = { ...localConfig, ...updates };
|
|
|
|
|
setLocalConfig(newConfig);
|
2025-11-28 14:56:11 +09:00
|
|
|
handleConfigChange?.(newConfig);
|
2025-11-14 17:40:07 +09:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 새 섹션 추가
|
|
|
|
|
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} > {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>
|
|
|
|
|
|
2025-11-14 17:40:07 +09:00
|
|
|
{/* 조건별 섹션 설정 */}
|
|
|
|
|
<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
|
2025-11-14 18:13:28 +09:00
|
|
|
value={section.screenId?.toString() || "none"}
|
2025-11-14 17:40:07 +09:00
|
|
|
onValueChange={(value) => {
|
2025-11-14 18:13:28 +09:00
|
|
|
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,
|
|
|
|
|
});
|
|
|
|
|
}
|
2025-11-14 17:40:07 +09:00
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<SelectTrigger className="h-7 text-xs">
|
|
|
|
|
<SelectValue placeholder="화면 선택..." />
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
2025-11-14 18:13:28 +09:00
|
|
|
<SelectItem value="none">선택 안 함</SelectItem>
|
2025-11-14 17:40:07 +09:00
|
|
|
{screens.map((screen) => (
|
|
|
|
|
<SelectItem
|
2025-11-14 18:00:56 +09:00
|
|
|
key={screen.screenId}
|
|
|
|
|
value={screen.screenId.toString()}
|
2025-11-14 17:40:07 +09:00
|
|
|
>
|
|
|
|
|
{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>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|