커밋 메세지 메뉴별 대중소 정리
This commit is contained in:
@@ -0,0 +1,339 @@
|
||||
"use client";
|
||||
|
||||
import React, { 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 {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import {
|
||||
Plus,
|
||||
Search,
|
||||
MoreHorizontal,
|
||||
Edit,
|
||||
Trash2,
|
||||
Play,
|
||||
History,
|
||||
RefreshCw
|
||||
} from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { CollectionAPI, DataCollectionConfig } from "@/lib/api/collection";
|
||||
import CollectionConfigModal from "@/components/admin/CollectionConfigModal";
|
||||
|
||||
export default function CollectionManagementPage() {
|
||||
const [configs, setConfigs] = useState<DataCollectionConfig[]>([]);
|
||||
const [filteredConfigs, setFilteredConfigs] = useState<DataCollectionConfig[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [statusFilter, setStatusFilter] = useState("all");
|
||||
const [typeFilter, setTypeFilter] = useState("all");
|
||||
|
||||
// 모달 상태
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [selectedConfig, setSelectedConfig] = useState<DataCollectionConfig | null>(null);
|
||||
|
||||
const collectionTypeOptions = CollectionAPI.getCollectionTypeOptions();
|
||||
|
||||
useEffect(() => {
|
||||
loadConfigs();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
filterConfigs();
|
||||
}, [configs, searchTerm, statusFilter, typeFilter]);
|
||||
|
||||
const loadConfigs = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const data = await CollectionAPI.getCollectionConfigs();
|
||||
setConfigs(data);
|
||||
} catch (error) {
|
||||
console.error("수집 설정 목록 조회 오류:", error);
|
||||
toast.error("수집 설정 목록을 불러오는데 실패했습니다.");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const filterConfigs = () => {
|
||||
let filtered = configs;
|
||||
|
||||
// 검색어 필터
|
||||
if (searchTerm) {
|
||||
filtered = filtered.filter(config =>
|
||||
config.config_name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
config.source_table.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
config.description?.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
}
|
||||
|
||||
// 상태 필터
|
||||
if (statusFilter !== "all") {
|
||||
filtered = filtered.filter(config => config.is_active === statusFilter);
|
||||
}
|
||||
|
||||
// 타입 필터
|
||||
if (typeFilter !== "all") {
|
||||
filtered = filtered.filter(config => config.collection_type === typeFilter);
|
||||
}
|
||||
|
||||
setFilteredConfigs(filtered);
|
||||
};
|
||||
|
||||
const handleCreate = () => {
|
||||
setSelectedConfig(null);
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
const handleEdit = (config: DataCollectionConfig) => {
|
||||
setSelectedConfig(config);
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
const handleDelete = async (config: DataCollectionConfig) => {
|
||||
if (!confirm(`"${config.config_name}" 수집 설정을 삭제하시겠습니까?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await CollectionAPI.deleteCollectionConfig(config.id!);
|
||||
toast.success("수집 설정이 삭제되었습니다.");
|
||||
loadConfigs();
|
||||
} catch (error) {
|
||||
console.error("수집 설정 삭제 오류:", error);
|
||||
toast.error("수집 설정 삭제에 실패했습니다.");
|
||||
}
|
||||
};
|
||||
|
||||
const handleExecute = async (config: DataCollectionConfig) => {
|
||||
try {
|
||||
await CollectionAPI.executeCollection(config.id!);
|
||||
toast.success(`"${config.config_name}" 수집 작업을 시작했습니다.`);
|
||||
} catch (error) {
|
||||
console.error("수집 작업 실행 오류:", error);
|
||||
toast.error("수집 작업 실행에 실패했습니다.");
|
||||
}
|
||||
};
|
||||
|
||||
const handleModalSave = () => {
|
||||
loadConfigs();
|
||||
};
|
||||
|
||||
const getStatusBadge = (isActive: string) => {
|
||||
return isActive === "Y" ? (
|
||||
<Badge className="bg-green-100 text-green-800">활성</Badge>
|
||||
) : (
|
||||
<Badge className="bg-red-100 text-red-800">비활성</Badge>
|
||||
);
|
||||
};
|
||||
|
||||
const getTypeBadge = (type: string) => {
|
||||
const option = collectionTypeOptions.find(opt => opt.value === type);
|
||||
const colors = {
|
||||
full: "bg-blue-100 text-blue-800",
|
||||
incremental: "bg-purple-100 text-purple-800",
|
||||
delta: "bg-orange-100 text-orange-800",
|
||||
};
|
||||
return (
|
||||
<Badge className={colors[type as keyof typeof colors] || "bg-gray-100 text-gray-800"}>
|
||||
{option?.label || type}
|
||||
</Badge>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className="w-full max-w-none px-4 py-8 space-y-8">
|
||||
{/* 헤더 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">수집 관리</h1>
|
||||
<p className="text-muted-foreground">
|
||||
외부 데이터베이스에서 데이터를 수집하는 설정을 관리합니다.
|
||||
</p>
|
||||
</div>
|
||||
<Button onClick={handleCreate}>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
새 수집 설정
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 필터 및 검색 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>필터 및 검색</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-col md:flex-row gap-4">
|
||||
<div className="flex-1">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="설정명, 테이블명, 설명으로 검색..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
||||
<SelectTrigger className="w-32">
|
||||
<SelectValue placeholder="상태" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">전체</SelectItem>
|
||||
<SelectItem value="Y">활성</SelectItem>
|
||||
<SelectItem value="N">비활성</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Select value={typeFilter} onValueChange={setTypeFilter}>
|
||||
<SelectTrigger className="w-40">
|
||||
<SelectValue placeholder="수집 타입" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">전체 타입</SelectItem>
|
||||
{collectionTypeOptions.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Button variant="outline" onClick={loadConfigs} disabled={isLoading}>
|
||||
<RefreshCw className={`h-4 w-4 mr-2 ${isLoading ? 'animate-spin' : ''}`} />
|
||||
새로고침
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 수집 설정 목록 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>수집 설정 목록 ({filteredConfigs.length}개)</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{isLoading ? (
|
||||
<div className="text-center py-8">
|
||||
<RefreshCw className="h-8 w-8 animate-spin mx-auto mb-2" />
|
||||
<p>수집 설정을 불러오는 중...</p>
|
||||
</div>
|
||||
) : filteredConfigs.length === 0 ? (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
{configs.length === 0 ? "수집 설정이 없습니다." : "검색 결과가 없습니다."}
|
||||
</div>
|
||||
) : (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">설정명</TableHead>
|
||||
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">수집 타입</TableHead>
|
||||
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">소스 테이블</TableHead>
|
||||
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">대상 테이블</TableHead>
|
||||
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">스케줄</TableHead>
|
||||
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">상태</TableHead>
|
||||
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">마지막 수집</TableHead>
|
||||
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">작업</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredConfigs.map((config) => (
|
||||
<TableRow key={config.id} className="bg-background transition-colors hover:bg-muted/50">
|
||||
<TableCell className="h-16 px-6 py-3 text-sm">
|
||||
<div>
|
||||
<div className="font-medium">{config.config_name}</div>
|
||||
{config.description && (
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{config.description}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="h-16 px-6 py-3 text-sm">
|
||||
{getTypeBadge(config.collection_type)}
|
||||
</TableCell>
|
||||
<TableCell className="h-16 px-6 py-3 font-mono text-sm">
|
||||
{config.source_table}
|
||||
</TableCell>
|
||||
<TableCell className="h-16 px-6 py-3 font-mono text-sm">
|
||||
{config.target_table || "-"}
|
||||
</TableCell>
|
||||
<TableCell className="h-16 px-6 py-3 font-mono text-sm">
|
||||
{config.schedule_cron || "-"}
|
||||
</TableCell>
|
||||
<TableCell className="h-16 px-6 py-3 text-sm">
|
||||
{getStatusBadge(config.is_active)}
|
||||
</TableCell>
|
||||
<TableCell className="h-16 px-6 py-3 text-sm">
|
||||
{config.last_collected_at
|
||||
? new Date(config.last_collected_at).toLocaleString()
|
||||
: "-"}
|
||||
</TableCell>
|
||||
<TableCell className="h-16 px-6 py-3 text-sm">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => handleEdit(config)}>
|
||||
<Edit className="h-4 w-4 mr-2" />
|
||||
수정
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleExecute(config)}
|
||||
disabled={config.is_active !== "Y"}
|
||||
>
|
||||
<Play className="h-4 w-4 mr-2" />
|
||||
실행
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleDelete(config)}>
|
||||
<Trash2 className="h-4 w-4 mr-2" />
|
||||
삭제
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 수집 설정 모달 */}
|
||||
<CollectionConfigModal
|
||||
isOpen={isModalOpen}
|
||||
onClose={() => setIsModalOpen(false)}
|
||||
onSave={handleModalSave}
|
||||
config={selectedConfig}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
"use client";
|
||||
|
||||
import { CodeCategoryPanel } from "@/components/admin/CodeCategoryPanel";
|
||||
import { CodeDetailPanel } from "@/components/admin/CodeDetailPanel";
|
||||
import { useSelectedCategory } from "@/hooks/useSelectedCategory";
|
||||
import { ScrollToTop } from "@/components/common/ScrollToTop";
|
||||
|
||||
export default function CommonCodeManagementPage() {
|
||||
const { selectedCategoryCode, selectCategory } = useSelectedCategory();
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col bg-background">
|
||||
<div className="space-y-6 p-6">
|
||||
{/* 페이지 헤더 */}
|
||||
<div className="space-y-2 border-b pb-4">
|
||||
<h1 className="text-3xl font-bold tracking-tight">공통코드 관리</h1>
|
||||
<p className="text-sm text-muted-foreground">시스템에서 사용하는 공통코드를 관리합니다</p>
|
||||
</div>
|
||||
|
||||
{/* 메인 콘텐츠 - 좌우 레이아웃 */}
|
||||
<div className="flex flex-col gap-6 lg:flex-row lg:gap-6">
|
||||
{/* 좌측: 카테고리 패널 */}
|
||||
<div className="w-full lg:w-80 lg:border-r lg:pr-6">
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-lg font-semibold">코드 카테고리</h2>
|
||||
<CodeCategoryPanel selectedCategoryCode={selectedCategoryCode} onSelectCategory={selectCategory} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 우측: 코드 상세 패널 */}
|
||||
<div className="min-w-0 flex-1 lg:pl-0">
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-lg font-semibold">
|
||||
코드 상세 정보
|
||||
{selectedCategoryCode && (
|
||||
<span className="ml-2 text-sm font-normal text-muted-foreground">({selectedCategoryCode})</span>
|
||||
)}
|
||||
</h2>
|
||||
<CodeDetailPanel categoryCode={selectedCategoryCode} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Scroll to Top 버튼 */}
|
||||
<ScrollToTop />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
"use client";
|
||||
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import { useState, useEffect } from "react";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { DataFlowDesigner } from "@/components/dataflow/DataFlowDesigner";
|
||||
import { DataFlowAPI } from "@/lib/api/dataflow";
|
||||
|
||||
export default function DataFlowEditPage() {
|
||||
const params = useParams();
|
||||
const router = useRouter();
|
||||
const [diagramId, setDiagramId] = useState<number>(0);
|
||||
const [diagramName, setDiagramName] = useState<string>("");
|
||||
|
||||
useEffect(() => {
|
||||
if (params.diagramId) {
|
||||
// URL에서 diagram_id 설정
|
||||
const id = parseInt(params.diagramId as string);
|
||||
setDiagramId(id);
|
||||
|
||||
// diagram_id로 관계도명 조회
|
||||
const fetchDiagramName = async () => {
|
||||
try {
|
||||
const jsonDiagram = await DataFlowAPI.getJsonDataFlowDiagramById(id);
|
||||
if (jsonDiagram && jsonDiagram.diagram_name) {
|
||||
setDiagramName(jsonDiagram.diagram_name);
|
||||
} else {
|
||||
setDiagramName(`관계도 ID: ${id}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("관계도명 조회 실패:", error);
|
||||
setDiagramName(`관계도 ID: ${id}`);
|
||||
}
|
||||
};
|
||||
|
||||
fetchDiagramName();
|
||||
}
|
||||
}, [params.diagramId]);
|
||||
|
||||
const handleBackToList = () => {
|
||||
router.push("/admin/dataflow");
|
||||
};
|
||||
|
||||
// 관계도 이름 업데이트 핸들러
|
||||
const handleDiagramNameUpdate = (newDiagramName: string) => {
|
||||
setDiagramName(newDiagramName);
|
||||
};
|
||||
|
||||
if (!diagramId || !diagramName) {
|
||||
return (
|
||||
<div className="flex h-64 items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="mx-auto mb-4 h-8 w-8 animate-spin rounded-full border-b-2 border-blue-600"></div>
|
||||
<p className="text-gray-500">관계도 정보를 불러오는 중...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
{/* 헤더 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-4">
|
||||
<Button variant="outline" size="sm" onClick={handleBackToList} className="flex items-center space-x-2">
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
<span>목록으로</span>
|
||||
</Button>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900">📊 관계도 편집</h1>
|
||||
<p className="mt-1 text-gray-600">
|
||||
<span className="font-medium text-blue-600">{diagramName}</span> 관계도를 편집하고 있습니다
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 데이터플로우 디자이너 */}
|
||||
<div className="rounded-lg border border-gray-200 bg-white">
|
||||
<DataFlowDesigner
|
||||
key={diagramId}
|
||||
selectedDiagram={diagramName}
|
||||
diagramId={diagramId}
|
||||
onBackToList={handleBackToList}
|
||||
onDiagramNameUpdate={handleDiagramNameUpdate}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* 제어 시스템 페이지 (리다이렉트)
|
||||
* 이 페이지는 /admin/dataflow로 리다이렉트됩니다.
|
||||
*/
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
export default function NodeEditorPage() {
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
// /admin/dataflow 메인 페이지로 리다이렉트
|
||||
router.replace("/admin/dataflow");
|
||||
}, [router]);
|
||||
|
||||
return (
|
||||
<div className="flex h-screen items-center justify-center bg-gray-50">
|
||||
<div className="text-gray-500">제어 관리 페이지로 이동중...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import DataFlowList from "@/components/dataflow/DataFlowList";
|
||||
import { FlowEditor } from "@/components/dataflow/node-editor/FlowEditor";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import { toast } from "sonner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ScrollToTop } from "@/components/common/ScrollToTop";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
|
||||
type Step = "list" | "editor";
|
||||
|
||||
export default function DataFlowPage() {
|
||||
const { user } = useAuth();
|
||||
const router = useRouter();
|
||||
const [currentStep, setCurrentStep] = useState<Step>("list");
|
||||
const [loadingFlowId, setLoadingFlowId] = useState<number | null>(null);
|
||||
|
||||
// 플로우 불러오기 핸들러
|
||||
const handleLoadFlow = async (flowId: number | null) => {
|
||||
if (flowId === null) {
|
||||
// 새 플로우 생성
|
||||
setLoadingFlowId(null);
|
||||
setCurrentStep("editor");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 기존 플로우 불러오기
|
||||
setLoadingFlowId(flowId);
|
||||
setCurrentStep("editor");
|
||||
|
||||
toast.success("플로우를 불러왔습니다.");
|
||||
} catch (error: any) {
|
||||
console.error("❌ 플로우 불러오기 실패:", error);
|
||||
toast.error(error.message || "플로우를 불러오는데 실패했습니다.");
|
||||
}
|
||||
};
|
||||
|
||||
// 목록으로 돌아가기
|
||||
const handleBackToList = () => {
|
||||
setCurrentStep("list");
|
||||
setLoadingFlowId(null);
|
||||
};
|
||||
|
||||
// 에디터 모드일 때는 전체 화면 사용
|
||||
const isEditorMode = currentStep === "editor";
|
||||
|
||||
// 에디터 모드일 때는 레이아웃 없이 전체 화면 사용
|
||||
if (isEditorMode) {
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 bg-background">
|
||||
<div className="flex h-full flex-col">
|
||||
{/* 에디터 헤더 */}
|
||||
<div className="flex items-center gap-4 border-b bg-background p-4">
|
||||
<Button variant="outline" size="sm" onClick={handleBackToList} className="flex items-center gap-2">
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
목록으로
|
||||
</Button>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold tracking-tight">노드 플로우 에디터</h1>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
드래그 앤 드롭으로 데이터 제어 플로우를 시각적으로 설계합니다
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 플로우 에디터 */}
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<FlowEditor key={loadingFlowId || "new"} initialFlowId={loadingFlowId} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col bg-background">
|
||||
<div className="space-y-6 p-4 sm:p-6">
|
||||
{/* 페이지 헤더 */}
|
||||
<div className="space-y-2 border-b pb-4">
|
||||
<h1 className="text-3xl font-bold tracking-tight">제어 관리</h1>
|
||||
<p className="text-sm text-muted-foreground">노드 기반 데이터 플로우를 시각적으로 설계하고 관리합니다</p>
|
||||
</div>
|
||||
|
||||
{/* 플로우 목록 */}
|
||||
<DataFlowList onLoadFlow={handleLoadFlow} />
|
||||
</div>
|
||||
|
||||
{/* Scroll to Top 버튼 */}
|
||||
<ScrollToTop />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import MultiLang from "@/components/admin/MultiLang";
|
||||
|
||||
export default function I18nPage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className="w-full max-w-none px-4 py-8">
|
||||
<MultiLang />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user