- Integrated multi-language functionality across various user management components, including user list, roles list, and user authorization pages, enhancing accessibility for diverse users. - Updated UI elements to utilize translation keys, ensuring that all text is dynamically translated based on user preferences. - Improved error handling messages to be localized, providing a better user experience in case of issues. These changes significantly enhance the usability and internationalization of the user management features, making the application more inclusive.
201 lines
5.6 KiB
TypeScript
201 lines
5.6 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect } from "react";
|
|
import { ChevronRight, ChevronDown, Folder, FolderOpen, Tag } from "lucide-react";
|
|
import { cn } from "@/lib/utils";
|
|
import { LangCategory, getCategories } from "@/lib/api/multilang";
|
|
|
|
interface CategoryTreeProps {
|
|
selectedCategoryId: number | null;
|
|
onSelectCategory: (category: LangCategory | null) => void;
|
|
onDoubleClickCategory?: (category: LangCategory) => void;
|
|
}
|
|
|
|
interface CategoryNodeProps {
|
|
category: LangCategory;
|
|
level: number;
|
|
selectedCategoryId: number | null;
|
|
onSelectCategory: (category: LangCategory) => void;
|
|
onDoubleClickCategory?: (category: LangCategory) => void;
|
|
}
|
|
|
|
function CategoryNode({
|
|
category,
|
|
level,
|
|
selectedCategoryId,
|
|
onSelectCategory,
|
|
onDoubleClickCategory,
|
|
}: CategoryNodeProps) {
|
|
// 기본값: 접힌 상태로 시작
|
|
const [isExpanded, setIsExpanded] = useState(false);
|
|
const hasChildren = category.children && category.children.length > 0;
|
|
const isSelected = selectedCategoryId === category.categoryId;
|
|
|
|
return (
|
|
<div>
|
|
<div
|
|
className={cn(
|
|
"flex cursor-pointer items-center gap-1 rounded-md px-2 py-1.5 text-sm transition-colors",
|
|
isSelected
|
|
? "border-l-[3px] border-l-blue-500 bg-blue-50 font-medium text-blue-700 dark:bg-blue-950 dark:text-blue-300"
|
|
: "border-l-[3px] border-l-transparent hover:bg-muted"
|
|
)}
|
|
style={{ paddingLeft: `${level * 16 + 8}px` }}
|
|
onClick={() => onSelectCategory(category)}
|
|
onDoubleClick={() => onDoubleClickCategory?.(category)}
|
|
>
|
|
{/* 확장/축소 아이콘 */}
|
|
{hasChildren ? (
|
|
<button
|
|
className="shrink-0"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
setIsExpanded(!isExpanded);
|
|
}}
|
|
>
|
|
{isExpanded ? (
|
|
<ChevronDown className="h-4 w-4" />
|
|
) : (
|
|
<ChevronRight className="h-4 w-4" />
|
|
)}
|
|
</button>
|
|
) : (
|
|
<span className="w-4" />
|
|
)}
|
|
|
|
{/* 폴더/태그 아이콘 */}
|
|
{hasChildren || level === 0 ? (
|
|
isExpanded ? (
|
|
<FolderOpen className="h-4 w-4 shrink-0 text-amber-500" />
|
|
) : (
|
|
<Folder className="h-4 w-4 shrink-0 text-amber-500" />
|
|
)
|
|
) : (
|
|
<Tag className="h-4 w-4 shrink-0 text-primary" />
|
|
)}
|
|
|
|
{/* 카테고리 이름 */}
|
|
<span className="truncate">{category.categoryName}</span>
|
|
|
|
{/* prefix 표시 */}
|
|
<span
|
|
className={cn(
|
|
"ml-auto text-xs",
|
|
isSelected ? "text-blue-500 dark:text-blue-400" : "text-muted-foreground"
|
|
)}
|
|
>
|
|
{category.keyPrefix}
|
|
</span>
|
|
</div>
|
|
|
|
{/* 자식 카테고리 */}
|
|
{hasChildren && isExpanded && (
|
|
<div>
|
|
{category.children!.map((child) => (
|
|
<CategoryNode
|
|
key={child.categoryId}
|
|
category={child}
|
|
level={level + 1}
|
|
selectedCategoryId={selectedCategoryId}
|
|
onSelectCategory={onSelectCategory}
|
|
onDoubleClickCategory={onDoubleClickCategory}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function CategoryTree({
|
|
selectedCategoryId,
|
|
onSelectCategory,
|
|
onDoubleClickCategory,
|
|
}: CategoryTreeProps) {
|
|
const [categories, setCategories] = useState<LangCategory[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
loadCategories();
|
|
}, []);
|
|
|
|
const loadCategories = async () => {
|
|
try {
|
|
setLoading(true);
|
|
const response = await getCategories();
|
|
if (response.success && response.data) {
|
|
setCategories(response.data);
|
|
} else {
|
|
setError(response.error?.details || "카테고리 로드 실패");
|
|
}
|
|
} catch (err) {
|
|
setError("카테고리 로드 중 오류 발생");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="flex h-32 items-center justify-center">
|
|
<div className="animate-pulse text-sm text-muted-foreground">
|
|
카테고리 로딩 중...
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<div className="flex h-32 items-center justify-center">
|
|
<div className="text-sm text-destructive">{error}</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (categories.length === 0) {
|
|
return (
|
|
<div className="flex h-32 items-center justify-center">
|
|
<div className="text-sm text-muted-foreground">
|
|
카테고리가 없습니다
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-0.5">
|
|
{/* 전체 선택 옵션 */}
|
|
<div
|
|
className={cn(
|
|
"flex cursor-pointer items-center gap-2 rounded-md px-2 py-1.5 text-sm transition-colors",
|
|
selectedCategoryId === null
|
|
? "border-l-[3px] border-l-blue-500 bg-blue-50 font-medium text-blue-700 dark:bg-blue-950 dark:text-blue-300"
|
|
: "border-l-[3px] border-l-transparent hover:bg-muted"
|
|
)}
|
|
onClick={() => onSelectCategory(null)}
|
|
>
|
|
<Folder className="h-4 w-4 shrink-0" />
|
|
<span>전체</span>
|
|
</div>
|
|
|
|
{/* 카테고리 트리 */}
|
|
{categories.map((category) => (
|
|
<CategoryNode
|
|
key={category.categoryId}
|
|
category={category}
|
|
level={0}
|
|
selectedCategoryId={selectedCategoryId}
|
|
onSelectCategory={onSelectCategory}
|
|
onDoubleClickCategory={onDoubleClickCategory}
|
|
/>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default CategoryTree;
|
|
|
|
|