ERP-node/frontend/components/screen/TemplateManager.tsx

387 lines
15 KiB
TypeScript
Raw Normal View History

2025-09-01 11:48:12 +09:00
"use client";
import { useState, useEffect } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Separator } from "@/components/ui/separator";
import { Search, Plus, Download, Upload, Trash2, Eye, Edit } from "lucide-react";
import { ScreenTemplate, LayoutData } from "@/types/screen";
import { templateApi } from "@/lib/api/screen";
import { useAuth } from "@/hooks/useAuth";
interface TemplateManagerProps {
onTemplateSelect?: (template: ScreenTemplate) => void;
onTemplateApply?: (template: ScreenTemplate) => void;
className?: string;
}
export default function TemplateManager({ onTemplateSelect, onTemplateApply, className }: TemplateManagerProps) {
const { user } = useAuth();
const [templates, setTemplates] = useState<ScreenTemplate[]>([]);
const [selectedTemplate, setSelectedTemplate] = useState<ScreenTemplate | null>(null);
const [searchTerm, setSearchTerm] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [activeTab, setActiveTab] = useState("my");
// 템플릿 목록 조회
useEffect(() => {
const fetchTemplates = async () => {
try {
setIsLoading(true);
const templateList = await templateApi.getTemplates({
companyCode: user?.company_code || "*",
});
setTemplates(templateList);
} catch (error) {
console.error("템플릿 목록 조회 실패:", error);
// API 호출 실패 시 기본 템플릿 목록 사용
const fallbackTemplates: ScreenTemplate[] = [
{
templateId: 1,
templateName: "기본 CRUD 화면",
templateType: "CRUD",
companyCode: "*",
description: "기본적인 CRUD 기능을 제공하는 화면 템플릿",
layoutData: {
components: [
{
id: "search-container",
type: "container",
position: { x: 0, y: 0 },
size: { width: 12, height: 100 },
title: "검색 영역",
children: [],
},
{
id: "table-container",
type: "container",
position: { x: 0, y: 1 },
size: { width: 12, height: 300 },
title: "데이터 테이블",
children: [],
},
{
id: "form-container",
type: "container",
position: { x: 0, y: 2 },
size: { width: 12, height: 200 },
title: "입력 폼",
children: [],
},
],
gridSettings: { columns: 12, gap: 16, padding: 16 },
},
isPublic: true,
createdBy: "admin",
createdDate: new Date(),
},
{
templateId: 2,
templateName: "목록 화면",
templateType: "LIST",
companyCode: "*",
description: "데이터 목록을 표시하는 화면 템플릿",
layoutData: {
components: [
{
id: "filter-container",
type: "container",
position: { x: 0, y: 0 },
size: { width: 12, height: 80 },
title: "필터 영역",
children: [],
},
{
id: "list-container",
type: "container",
position: { x: 0, y: 1 },
size: { width: 12, height: 400 },
title: "목록 영역",
children: [],
},
],
gridSettings: { columns: 12, gap: 16, padding: 16 },
},
isPublic: true,
createdBy: "admin",
createdDate: new Date(),
},
{
templateId: 3,
templateName: "상세 화면",
templateType: "DETAIL",
companyCode: "*",
description: "데이터 상세 정보를 표시하는 화면 템플릿",
layoutData: {
components: [
{
id: "header-container",
type: "container",
position: { x: 0, y: 0 },
size: { width: 12, height: 60 },
title: "헤더 영역",
children: [],
},
{
id: "detail-container",
type: "container",
position: { x: 0, y: 1 },
size: { width: 12, height: 400 },
title: "상세 정보",
children: [],
},
{
id: "action-container",
type: "container",
position: { x: 0, y: 2 },
size: { width: 12, height: 80 },
title: "액션 버튼",
children: [],
},
],
gridSettings: { columns: 12, gap: 16, padding: 16 },
},
isPublic: true,
createdBy: "admin",
createdDate: new Date(),
},
];
setTemplates(fallbackTemplates);
} finally {
setIsLoading(false);
}
};
fetchTemplates();
}, [user?.company_code]);
// 템플릿 검색
const filteredTemplates = templates.filter(
(template) =>
template.templateName.toLowerCase().includes(searchTerm.toLowerCase()) ||
(template.description || "").toLowerCase().includes(searchTerm.toLowerCase()),
);
// 템플릿 선택
const handleTemplateSelect = (template: ScreenTemplate) => {
setSelectedTemplate(template);
onTemplateSelect?.(template);
};
// 템플릿 적용
const handleTemplateApply = (template: ScreenTemplate) => {
onTemplateApply?.(template);
};
// 템플릿 삭제
const handleTemplateDelete = async (templateId: number) => {
if (!confirm("정말로 이 템플릿을 삭제하시겠습니까?")) return;
try {
await templateApi.deleteTemplate(templateId);
setTemplates((prev) => prev.filter((t) => t.templateId !== templateId));
if (selectedTemplate?.templateId === templateId) {
setSelectedTemplate(null);
}
alert("템플릿이 삭제되었습니다.");
} catch (error) {
console.error("템플릿 삭제 실패:", error);
alert("템플릿 삭제에 실패했습니다. 다시 시도해주세요.");
}
};
// 새 템플릿 생성
const handleCreateTemplate = () => {
// TODO: 새 템플릿 생성 모달 또는 페이지로 이동
console.log("새 템플릿 생성");
};
// 템플릿 내보내기
const handleExportTemplate = (template: ScreenTemplate) => {
const dataStr = JSON.stringify(template, null, 2);
const dataBlob = new Blob([dataStr], { type: "application/json" });
const url = URL.createObjectURL(dataBlob);
const link = document.createElement("a");
link.href = url;
link.download = `${template.templateName}.json`;
link.click();
URL.revokeObjectURL(url);
};
return (
<div className={`space-y-4 ${className}`}>
{/* 헤더 */}
<Card>
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<CardTitle className="text-sm font-medium"> 릿 </CardTitle>
<div className="flex items-center gap-2">
<Button variant="outline" size="sm" onClick={handleCreateTemplate}>
<Plus className="mr-1 h-3 w-3" /> 릿
</Button>
<Button variant="outline" size="sm">
<Upload className="mr-1 h-3 w-3" />
</Button>
</div>
</div>
</CardHeader>
<CardContent>
<div className="relative">
<Search className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-gray-400" />
<Input
placeholder="템플릿명 또는 설명으로 검색..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
</CardContent>
</Card>
{/* 템플릿 목록 */}
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
{/* 템플릿 카드 목록 */}
<div className="space-y-3">
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="my"> 릿</TabsTrigger>
<TabsTrigger value="public"> 릿</TabsTrigger>
</TabsList>
</Tabs>
<div className="max-h-96 space-y-3 overflow-y-auto">
{filteredTemplates
.filter((template) => (activeTab === "my" ? template.companyCode !== "*" : template.isPublic))
.map((template) => (
<Card
key={template.templateId}
className={`cursor-pointer transition-all hover:shadow-md ${
selectedTemplate?.templateId === template.templateId ? "ring-2 ring-blue-500" : ""
}`}
onClick={() => handleTemplateSelect(template)}
>
<CardContent className="p-4">
<div className="flex items-start justify-between">
<div className="flex-1">
<h3 className="mb-1 text-sm font-medium">{template.templateName}</h3>
<p className="mb-2 text-xs text-gray-600">{template.description || "설명 없음"}</p>
<div className="flex items-center gap-2">
<Badge variant="outline" className="text-xs">
{template.templateType}
</Badge>
{template.isPublic && (
<Badge variant="secondary" className="text-xs">
</Badge>
)}
<span className="text-xs text-gray-500">
{template.createdBy} {template.createdDate.toLocaleDateString()}
</span>
</div>
</div>
<div className="flex items-center gap-1">
<Button
size="sm"
variant="ghost"
onClick={(e) => {
e.stopPropagation();
handleTemplateApply(template);
}}
>
<Eye className="h-3 w-3" />
</Button>
<Button
size="sm"
variant="ghost"
onClick={(e) => {
e.stopPropagation();
handleExportTemplate(template);
}}
>
<Download className="h-3 w-3" />
</Button>
{template.companyCode !== "*" && (
<Button
size="sm"
variant="ghost"
onClick={(e) => {
e.stopPropagation();
handleTemplateDelete(template.templateId);
}}
className="text-red-600 hover:text-red-700"
>
<Trash2 className="h-3 w-3" />
</Button>
)}
</div>
</div>
</CardContent>
</Card>
))}
</div>
</div>
{/* 선택된 템플릿 상세 정보 */}
<div>
{selectedTemplate ? (
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm font-medium">릿 </CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div>
<label className="text-sm font-medium">릿</label>
<p className="text-sm text-gray-600">{selectedTemplate.templateName}</p>
</div>
<div>
<label className="text-sm font-medium"></label>
<p className="text-sm text-gray-600">{selectedTemplate.description}</p>
</div>
<div>
<label className="text-sm font-medium"></label>
<Badge variant="outline" className="text-xs">
{selectedTemplate.templateType}
</Badge>
</div>
<div>
<label className="text-sm font-medium"> </label>
<p className="text-sm text-gray-600">{selectedTemplate.layoutData?.components?.length || 0}</p>
</div>
<div>
<label className="text-sm font-medium"> </label>
<p className="text-sm text-gray-600">
{selectedTemplate.layoutData?.gridSettings?.columns || 12} , :{" "}
{selectedTemplate.layoutData?.gridSettings?.gap || 16}px
</p>
</div>
<Separator />
<div className="flex gap-2">
<Button className="flex-1" onClick={() => handleTemplateApply(selectedTemplate)}>
<Plus className="mr-1 h-3 w-3" />
릿
</Button>
<Button variant="outline" className="flex-1">
<Edit className="mr-1 h-3 w-3" />
</Button>
</div>
</CardContent>
</Card>
) : (
<Card>
<CardContent className="p-8 text-center text-gray-500">
<Eye className="mx-auto mb-4 h-12 w-12 text-gray-300" />
<p>릿 </p>
</CardContent>
</Card>
)}
</div>
</div>
</div>
);
}