721 lines
22 KiB
TypeScript
721 lines
22 KiB
TypeScript
"use client";
|
|
|
|
/**
|
|
* 카테고리 값 관리 - 트리 구조 버전
|
|
* - 3단계 트리 구조 지원 (대분류/중분류/소분류)
|
|
*/
|
|
|
|
import React, { useState, useEffect, useCallback } from "react";
|
|
import {
|
|
ChevronRight,
|
|
ChevronDown,
|
|
Plus,
|
|
Pencil,
|
|
Trash2,
|
|
Folder,
|
|
FolderOpen,
|
|
Tag,
|
|
Search,
|
|
RefreshCw,
|
|
} from "lucide-react";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { cn } from "@/lib/utils";
|
|
import {
|
|
CategoryValue,
|
|
getCategoryTree,
|
|
createCategoryValue,
|
|
updateCategoryValue,
|
|
deleteCategoryValue,
|
|
CreateCategoryValueInput,
|
|
} from "@/lib/api/categoryTree";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
DialogFooter,
|
|
DialogDescription,
|
|
} from "@/components/ui/dialog";
|
|
import {
|
|
AlertDialog,
|
|
AlertDialogAction,
|
|
AlertDialogCancel,
|
|
AlertDialogContent,
|
|
AlertDialogDescription,
|
|
AlertDialogFooter,
|
|
AlertDialogHeader,
|
|
AlertDialogTitle,
|
|
} from "@/components/ui/alert-dialog";
|
|
import { Label } from "@/components/ui/label";
|
|
import { toast } from "sonner";
|
|
import { Switch } from "@/components/ui/switch";
|
|
|
|
interface CategoryValueManagerTreeProps {
|
|
tableName: string;
|
|
columnName: string;
|
|
columnLabel: string;
|
|
onValueCountChange?: (count: number) => void;
|
|
}
|
|
|
|
// 트리 노드 컴포넌트
|
|
interface TreeNodeProps {
|
|
node: CategoryValue;
|
|
level: number;
|
|
expandedNodes: Set<number>;
|
|
selectedValueId?: number;
|
|
searchQuery: string;
|
|
onToggle: (valueId: number) => void;
|
|
onSelect: (value: CategoryValue) => void;
|
|
onAdd: (parentValue: CategoryValue | null) => void;
|
|
onEdit: (value: CategoryValue) => void;
|
|
onDelete: (value: CategoryValue) => void;
|
|
}
|
|
|
|
// 검색어가 노드 또는 하위에 매칭되는지 확인
|
|
const nodeMatchesSearch = (node: CategoryValue, query: string): boolean => {
|
|
if (!query) return true;
|
|
const lowerQuery = query.toLowerCase();
|
|
if (node.valueLabel.toLowerCase().includes(lowerQuery)) return true;
|
|
if (node.valueCode.toLowerCase().includes(lowerQuery)) return true;
|
|
if (node.children) {
|
|
return node.children.some((child) => nodeMatchesSearch(child, query));
|
|
}
|
|
return false;
|
|
};
|
|
|
|
const TreeNode: React.FC<TreeNodeProps> = ({
|
|
node,
|
|
level,
|
|
expandedNodes,
|
|
selectedValueId,
|
|
searchQuery,
|
|
onToggle,
|
|
onSelect,
|
|
onAdd,
|
|
onEdit,
|
|
onDelete,
|
|
}) => {
|
|
const hasChildren = node.children && node.children.length > 0;
|
|
const isExpanded = expandedNodes.has(node.valueId);
|
|
const isSelected = selectedValueId === node.valueId;
|
|
const canAddChild = node.depth < 3;
|
|
|
|
// 검색 필터링
|
|
if (searchQuery && !nodeMatchesSearch(node, searchQuery)) {
|
|
return null;
|
|
}
|
|
|
|
// 깊이별 아이콘
|
|
const getIcon = () => {
|
|
if (hasChildren) {
|
|
return isExpanded ? (
|
|
<FolderOpen className="h-4 w-4 text-amber-500" />
|
|
) : (
|
|
<Folder className="h-4 w-4 text-amber-500" />
|
|
);
|
|
}
|
|
return <Tag className="h-4 w-4 text-blue-500" />;
|
|
};
|
|
|
|
// 깊이별 라벨
|
|
const getDepthLabel = () => {
|
|
switch (node.depth) {
|
|
case 1:
|
|
return "대분류";
|
|
case 2:
|
|
return "중분류";
|
|
case 3:
|
|
return "소분류";
|
|
default:
|
|
return "";
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div>
|
|
<div
|
|
className={cn(
|
|
"group flex items-center gap-1 rounded-md px-2 py-2 transition-colors",
|
|
isSelected ? "border-primary bg-primary/10 border-l-2" : "hover:bg-muted/50",
|
|
"cursor-pointer",
|
|
)}
|
|
style={{ paddingLeft: `${level * 20 + 8}px` }}
|
|
onClick={() => onSelect(node)}
|
|
>
|
|
{/* 확장 토글 */}
|
|
<button
|
|
type="button"
|
|
className="hover:bg-muted flex h-6 w-6 items-center justify-center rounded"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
if (hasChildren) {
|
|
onToggle(node.valueId);
|
|
}
|
|
}}
|
|
>
|
|
{hasChildren ? (
|
|
isExpanded ? (
|
|
<ChevronDown className="text-muted-foreground h-4 w-4" />
|
|
) : (
|
|
<ChevronRight className="text-muted-foreground h-4 w-4" />
|
|
)
|
|
) : (
|
|
<span className="h-4 w-4" />
|
|
)}
|
|
</button>
|
|
|
|
{/* 아이콘 */}
|
|
{getIcon()}
|
|
|
|
{/* 라벨 */}
|
|
<div className="flex flex-1 items-center gap-2">
|
|
<span className={cn("text-sm", node.depth === 1 && "font-medium")}>{node.valueLabel}</span>
|
|
<span className="bg-muted text-muted-foreground rounded px-1.5 py-0.5 text-[10px]">{getDepthLabel()}</span>
|
|
</div>
|
|
|
|
{/* 비활성 표시 */}
|
|
{!node.isActive && (
|
|
<span className="bg-destructive/10 text-destructive rounded px-1.5 py-0.5 text-[10px]">비활성</span>
|
|
)}
|
|
|
|
{/* 액션 버튼 */}
|
|
<div className="flex items-center gap-0.5 opacity-0 transition-opacity group-hover:opacity-100">
|
|
{canAddChild && (
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-7 w-7"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
onAdd(node);
|
|
}}
|
|
title="하위 추가"
|
|
>
|
|
<Plus className="h-3.5 w-3.5" />
|
|
</Button>
|
|
)}
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-7 w-7"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
onEdit(node);
|
|
}}
|
|
title="수정"
|
|
>
|
|
<Pencil className="h-3.5 w-3.5" />
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="text-destructive hover:text-destructive h-7 w-7"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
onDelete(node);
|
|
}}
|
|
title="삭제"
|
|
>
|
|
<Trash2 className="h-3.5 w-3.5" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 자식 노드 */}
|
|
{hasChildren && isExpanded && (
|
|
<div>
|
|
{node.children!.map((child) => (
|
|
<TreeNode
|
|
key={child.valueId}
|
|
node={child}
|
|
level={level + 1}
|
|
expandedNodes={expandedNodes}
|
|
selectedValueId={selectedValueId}
|
|
searchQuery={searchQuery}
|
|
onToggle={onToggle}
|
|
onSelect={onSelect}
|
|
onAdd={onAdd}
|
|
onEdit={onEdit}
|
|
onDelete={onDelete}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export const CategoryValueManagerTree: React.FC<CategoryValueManagerTreeProps> = ({
|
|
tableName,
|
|
columnName,
|
|
columnLabel,
|
|
onValueCountChange,
|
|
}) => {
|
|
// 상태
|
|
const [tree, setTree] = useState<CategoryValue[]>([]);
|
|
const [loading, setLoading] = useState(false);
|
|
const [expandedNodes, setExpandedNodes] = useState<Set<number>>(new Set());
|
|
const [selectedValue, setSelectedValue] = useState<CategoryValue | null>(null);
|
|
const [searchQuery, setSearchQuery] = useState("");
|
|
const [showInactive, setShowInactive] = useState(false);
|
|
|
|
// 모달 상태
|
|
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
|
|
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
|
|
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
|
const [parentValue, setParentValue] = useState<CategoryValue | null>(null);
|
|
const [editingValue, setEditingValue] = useState<CategoryValue | null>(null);
|
|
const [deletingValue, setDeletingValue] = useState<CategoryValue | null>(null);
|
|
|
|
// 폼 상태
|
|
const [formData, setFormData] = useState({
|
|
valueCode: "",
|
|
valueLabel: "",
|
|
description: "",
|
|
color: "",
|
|
isActive: true,
|
|
});
|
|
|
|
// 전체 값 개수 계산
|
|
const countAllValues = useCallback((nodes: CategoryValue[]): number => {
|
|
let count = nodes.length;
|
|
for (const node of nodes) {
|
|
if (node.children) {
|
|
count += countAllValues(node.children);
|
|
}
|
|
}
|
|
return count;
|
|
}, []);
|
|
|
|
// 활성 노드만 필터링
|
|
const filterActiveNodes = useCallback((nodes: CategoryValue[]): CategoryValue[] => {
|
|
return nodes
|
|
.filter((node) => node.isActive !== false)
|
|
.map((node) => ({
|
|
...node,
|
|
children: node.children ? filterActiveNodes(node.children) : undefined,
|
|
}));
|
|
}, []);
|
|
|
|
// 데이터 로드
|
|
const loadTree = useCallback(async () => {
|
|
if (!tableName || !columnName) return;
|
|
|
|
setLoading(true);
|
|
try {
|
|
const response = await getCategoryTree(tableName, columnName);
|
|
if (response.success && response.data) {
|
|
let filteredTree = response.data;
|
|
|
|
// 비활성 필터링
|
|
if (!showInactive) {
|
|
filteredTree = filterActiveNodes(response.data);
|
|
}
|
|
|
|
setTree(filteredTree);
|
|
|
|
// 1단계 노드는 기본 펼침
|
|
const rootIds = new Set(filteredTree.map((n) => n.valueId));
|
|
setExpandedNodes(rootIds);
|
|
|
|
// 전체 개수 업데이트
|
|
const totalCount = countAllValues(response.data);
|
|
onValueCountChange?.(totalCount);
|
|
}
|
|
} catch (error) {
|
|
console.error("카테고리 트리 로드 오류:", error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [tableName, columnName, showInactive, countAllValues, filterActiveNodes, onValueCountChange]);
|
|
|
|
useEffect(() => {
|
|
loadTree();
|
|
}, [loadTree]);
|
|
|
|
// 모든 노드 펼치기
|
|
const expandAll = () => {
|
|
const allIds = new Set<number>();
|
|
const collectIds = (nodes: CategoryValue[]) => {
|
|
for (const node of nodes) {
|
|
allIds.add(node.valueId);
|
|
if (node.children) {
|
|
collectIds(node.children);
|
|
}
|
|
}
|
|
};
|
|
collectIds(tree);
|
|
setExpandedNodes(allIds);
|
|
};
|
|
|
|
// 모든 노드 접기
|
|
const collapseAll = () => {
|
|
setExpandedNodes(new Set());
|
|
};
|
|
|
|
// 토글 핸들러
|
|
const handleToggle = (valueId: number) => {
|
|
setExpandedNodes((prev) => {
|
|
const next = new Set(prev);
|
|
if (next.has(valueId)) {
|
|
next.delete(valueId);
|
|
} else {
|
|
next.add(valueId);
|
|
}
|
|
return next;
|
|
});
|
|
};
|
|
|
|
// 추가 모달 열기
|
|
const handleOpenAddModal = (parent: CategoryValue | null) => {
|
|
setParentValue(parent);
|
|
setFormData({
|
|
valueCode: "",
|
|
valueLabel: "",
|
|
description: "",
|
|
color: "",
|
|
isActive: true,
|
|
});
|
|
setIsAddModalOpen(true);
|
|
};
|
|
|
|
// 수정 모달 열기
|
|
const handleOpenEditModal = (value: CategoryValue) => {
|
|
setEditingValue(value);
|
|
setFormData({
|
|
valueCode: value.valueCode,
|
|
valueLabel: value.valueLabel,
|
|
description: value.description || "",
|
|
color: value.color || "",
|
|
isActive: value.isActive,
|
|
});
|
|
setIsEditModalOpen(true);
|
|
};
|
|
|
|
// 삭제 다이얼로그 열기
|
|
const handleOpenDeleteDialog = (value: CategoryValue) => {
|
|
setDeletingValue(value);
|
|
setIsDeleteDialogOpen(true);
|
|
};
|
|
|
|
// 코드 자동 생성 함수
|
|
const generateCode = () => {
|
|
const timestamp = Date.now().toString(36).toUpperCase();
|
|
const random = Math.random().toString(36).substring(2, 6).toUpperCase();
|
|
return `CAT_${timestamp}_${random}`;
|
|
};
|
|
|
|
// 추가 처리
|
|
const handleAdd = async () => {
|
|
if (!formData.valueLabel) {
|
|
toast.error("이름은 필수입니다");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// 코드 자동 생성
|
|
const autoCode = generateCode();
|
|
|
|
const input: CreateCategoryValueInput = {
|
|
tableName,
|
|
columnName,
|
|
valueCode: autoCode,
|
|
valueLabel: formData.valueLabel,
|
|
parentValueId: parentValue?.valueId || null,
|
|
description: formData.description || undefined,
|
|
color: formData.color || undefined,
|
|
isActive: formData.isActive,
|
|
};
|
|
|
|
const response = await createCategoryValue(input);
|
|
if (response.success) {
|
|
toast.success("카테고리가 추가되었습니다");
|
|
setIsAddModalOpen(false);
|
|
loadTree();
|
|
if (parentValue) {
|
|
setExpandedNodes((prev) => new Set([...prev, parentValue.valueId]));
|
|
}
|
|
} else {
|
|
toast.error(response.error || "추가 실패");
|
|
}
|
|
} catch (error) {
|
|
console.error("카테고리 추가 오류:", error);
|
|
toast.error("카테고리 추가 중 오류가 발생했습니다");
|
|
}
|
|
};
|
|
|
|
// 수정 처리
|
|
const handleEdit = async () => {
|
|
if (!editingValue) return;
|
|
|
|
try {
|
|
// 코드는 변경하지 않음 (기존 코드 유지)
|
|
const response = await updateCategoryValue(editingValue.valueId, {
|
|
valueLabel: formData.valueLabel,
|
|
description: formData.description || undefined,
|
|
color: formData.color || undefined,
|
|
isActive: formData.isActive,
|
|
});
|
|
|
|
if (response.success) {
|
|
toast.success("카테고리가 수정되었습니다");
|
|
setIsEditModalOpen(false);
|
|
loadTree();
|
|
} else {
|
|
toast.error(response.error || "수정 실패");
|
|
}
|
|
} catch (error) {
|
|
console.error("카테고리 수정 오류:", error);
|
|
toast.error("카테고리 수정 중 오류가 발생했습니다");
|
|
}
|
|
};
|
|
|
|
// 삭제 처리
|
|
const handleDelete = async () => {
|
|
if (!deletingValue) return;
|
|
|
|
try {
|
|
const response = await deleteCategoryValue(deletingValue.valueId);
|
|
if (response.success) {
|
|
toast.success("카테고리가 삭제되었습니다");
|
|
setIsDeleteDialogOpen(false);
|
|
setSelectedValue(null);
|
|
loadTree();
|
|
} else {
|
|
toast.error(response.error || "삭제 실패");
|
|
}
|
|
} catch (error) {
|
|
console.error("카테고리 삭제 오류:", error);
|
|
toast.error("카테고리 삭제 중 오류가 발생했습니다");
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="flex h-full flex-col">
|
|
{/* 헤더 */}
|
|
<div className="mb-3 flex items-center justify-between border-b pb-3">
|
|
<h3 className="text-base font-semibold">{columnLabel} 카테고리</h3>
|
|
<Button variant="default" size="sm" className="h-8 gap-1.5 text-xs" onClick={() => handleOpenAddModal(null)}>
|
|
<Plus className="h-3.5 w-3.5" />
|
|
대분류 추가
|
|
</Button>
|
|
</div>
|
|
|
|
{/* 툴바 */}
|
|
<div className="mb-3 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
|
{/* 검색 */}
|
|
<div className="relative max-w-xs flex-1">
|
|
<Search className="text-muted-foreground absolute left-2.5 top-1/2 h-4 w-4 -translate-y-1/2" />
|
|
<Input
|
|
placeholder="검색..."
|
|
value={searchQuery}
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
className="h-8 pl-8 text-sm"
|
|
/>
|
|
</div>
|
|
|
|
{/* 옵션 */}
|
|
<div className="flex items-center gap-3">
|
|
<div className="flex items-center gap-2">
|
|
<Switch id="showInactive" checked={showInactive} onCheckedChange={setShowInactive} />
|
|
<Label htmlFor="showInactive" className="cursor-pointer text-xs">
|
|
비활성 표시
|
|
</Label>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-1">
|
|
<Button variant="ghost" size="sm" className="h-7 text-xs" onClick={expandAll}>
|
|
전체 펼침
|
|
</Button>
|
|
<Button variant="ghost" size="sm" className="h-7 text-xs" onClick={collapseAll}>
|
|
전체 접기
|
|
</Button>
|
|
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={loadTree} title="새로고침">
|
|
<RefreshCw className="h-3.5 w-3.5" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 트리 */}
|
|
<div className="bg-card min-h-[300px] flex-1 overflow-y-auto rounded-md border">
|
|
{loading ? (
|
|
<div className="flex h-full items-center justify-center">
|
|
<div className="text-muted-foreground text-sm">로딩 중...</div>
|
|
</div>
|
|
) : tree.length === 0 ? (
|
|
<div className="flex h-full flex-col items-center justify-center p-6 text-center">
|
|
<Folder className="text-muted-foreground/30 mb-3 h-12 w-12" />
|
|
<p className="text-muted-foreground text-sm">카테고리가 없습니다</p>
|
|
<p className="text-muted-foreground mt-1 text-xs">상단의 대분류 추가 버튼을 클릭하여 시작하세요</p>
|
|
</div>
|
|
) : (
|
|
<div className="p-2">
|
|
{tree.map((node) => (
|
|
<TreeNode
|
|
key={node.valueId}
|
|
node={node}
|
|
level={0}
|
|
expandedNodes={expandedNodes}
|
|
selectedValueId={selectedValue?.valueId}
|
|
searchQuery={searchQuery}
|
|
onToggle={handleToggle}
|
|
onSelect={setSelectedValue}
|
|
onAdd={handleOpenAddModal}
|
|
onEdit={handleOpenEditModal}
|
|
onDelete={handleOpenDeleteDialog}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
|
|
{/* 추가 모달 */}
|
|
<Dialog open={isAddModalOpen} onOpenChange={setIsAddModalOpen}>
|
|
<DialogContent className="max-w-[95vw] sm:max-w-[450px]">
|
|
<DialogHeader>
|
|
<DialogTitle className="text-base sm:text-lg">
|
|
{parentValue ? `"${parentValue.valueLabel}" 하위 추가` : "대분류 추가"}
|
|
</DialogTitle>
|
|
<DialogDescription className="text-xs sm:text-sm">
|
|
{parentValue ? `${parentValue.depth + 1}단계 카테고리를 추가합니다` : "1단계 대분류 카테고리를 추가합니다"}
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<div className="space-y-4">
|
|
<div>
|
|
<Label htmlFor="valueLabel" className="text-xs sm:text-sm">
|
|
이름 <span className="text-destructive">*</span>
|
|
</Label>
|
|
<Input
|
|
id="valueLabel"
|
|
value={formData.valueLabel}
|
|
onChange={(e) => setFormData({ ...formData, valueLabel: e.target.value })}
|
|
placeholder="카테고리 이름을 입력하세요"
|
|
className="h-9 text-sm"
|
|
/>
|
|
<p className="text-muted-foreground mt-1 text-[10px]">코드는 자동으로 생성됩니다</p>
|
|
</div>
|
|
<div>
|
|
<Label htmlFor="description" className="text-xs sm:text-sm">
|
|
설명
|
|
</Label>
|
|
<Input
|
|
id="description"
|
|
value={formData.description}
|
|
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
|
|
placeholder="선택 사항"
|
|
className="h-9 text-sm"
|
|
/>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<Switch
|
|
id="isActive"
|
|
checked={formData.isActive}
|
|
onCheckedChange={(checked) => setFormData({ ...formData, isActive: checked })}
|
|
/>
|
|
<Label htmlFor="isActive" className="cursor-pointer text-sm">
|
|
활성 상태
|
|
</Label>
|
|
</div>
|
|
</div>
|
|
|
|
<DialogFooter className="gap-2 sm:gap-0">
|
|
<Button variant="outline" onClick={() => setIsAddModalOpen(false)} className="h-9 flex-1 text-sm sm:flex-none">
|
|
취소
|
|
</Button>
|
|
<Button onClick={handleAdd} className="h-9 flex-1 text-sm sm:flex-none">
|
|
추가
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
{/* 수정 모달 */}
|
|
<Dialog open={isEditModalOpen} onOpenChange={setIsEditModalOpen}>
|
|
<DialogContent className="max-w-[95vw] sm:max-w-[450px]">
|
|
<DialogHeader>
|
|
<DialogTitle className="text-base sm:text-lg">카테고리 수정</DialogTitle>
|
|
<DialogDescription className="text-xs sm:text-sm">카테고리 정보를 수정합니다</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<div className="space-y-4">
|
|
<div>
|
|
<Label htmlFor="editValueLabel" className="text-xs sm:text-sm">
|
|
이름 <span className="text-destructive">*</span>
|
|
</Label>
|
|
<Input
|
|
id="editValueLabel"
|
|
value={formData.valueLabel}
|
|
onChange={(e) => setFormData({ ...formData, valueLabel: e.target.value })}
|
|
className="h-9 text-sm"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label htmlFor="editDescription" className="text-xs sm:text-sm">
|
|
설명
|
|
</Label>
|
|
<Input
|
|
id="editDescription"
|
|
value={formData.description}
|
|
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
|
|
className="h-9 text-sm"
|
|
/>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<Switch
|
|
id="editIsActive"
|
|
checked={formData.isActive}
|
|
onCheckedChange={(checked) => setFormData({ ...formData, isActive: checked })}
|
|
/>
|
|
<Label htmlFor="editIsActive" className="cursor-pointer text-sm">
|
|
활성 상태
|
|
</Label>
|
|
</div>
|
|
</div>
|
|
|
|
<DialogFooter className="gap-2 sm:gap-0">
|
|
<Button variant="outline" onClick={() => setIsEditModalOpen(false)} className="h-9 flex-1 text-sm sm:flex-none">
|
|
취소
|
|
</Button>
|
|
<Button onClick={handleEdit} className="h-9 flex-1 text-sm sm:flex-none">
|
|
저장
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
{/* 삭제 확인 다이얼로그 */}
|
|
<AlertDialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
|
|
<AlertDialogContent>
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle>카테고리 삭제</AlertDialogTitle>
|
|
<AlertDialogDescription>
|
|
<strong>{deletingValue?.valueLabel}</strong>을(를) 삭제하시겠습니까?
|
|
{deletingValue?.children && deletingValue.children.length > 0 && (
|
|
<>
|
|
<br />
|
|
<span className="text-destructive">
|
|
하위 카테고리 {deletingValue.children.length}개도 모두 함께 삭제됩니다.
|
|
</span>
|
|
</>
|
|
)}
|
|
</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel>취소</AlertDialogCancel>
|
|
<AlertDialogAction onClick={handleDelete} className="bg-destructive text-destructive-foreground hover:bg-destructive/90">
|
|
삭제
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default CategoryValueManagerTree;
|
|
|