535 lines
19 KiB
TypeScript
535 lines
19 KiB
TypeScript
|
|
"use client";
|
||
|
|
|
||
|
|
import React, { useState } from "react";
|
||
|
|
import { Button } from "@/components/ui/button";
|
||
|
|
import { Input } from "@/components/ui/input";
|
||
|
|
import { Label } from "@/components/ui/label";
|
||
|
|
import { Textarea } from "@/components/ui/textarea";
|
||
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||
|
|
import {
|
||
|
|
Dialog,
|
||
|
|
DialogContent,
|
||
|
|
DialogDescription,
|
||
|
|
DialogFooter,
|
||
|
|
DialogHeader,
|
||
|
|
DialogTitle,
|
||
|
|
} from "@/components/ui/dialog";
|
||
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||
|
|
import { Badge } from "@/components/ui/badge";
|
||
|
|
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||
|
|
import {
|
||
|
|
Grid,
|
||
|
|
Layout,
|
||
|
|
Navigation,
|
||
|
|
Building,
|
||
|
|
FileText,
|
||
|
|
Table,
|
||
|
|
LayoutDashboard,
|
||
|
|
Plus,
|
||
|
|
Minus,
|
||
|
|
Info,
|
||
|
|
Wand2,
|
||
|
|
} from "lucide-react";
|
||
|
|
import { LayoutCategory } from "@/types/layout";
|
||
|
|
import { toast } from "sonner";
|
||
|
|
|
||
|
|
interface LayoutFormModalProps {
|
||
|
|
open: boolean;
|
||
|
|
onOpenChange: (open: boolean) => void;
|
||
|
|
onSuccess: () => void;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 카테고리 정의
|
||
|
|
const CATEGORIES = [
|
||
|
|
{ id: "basic", name: "기본", icon: Grid, description: "그리드, 플렉스박스 등 기본 레이아웃" },
|
||
|
|
{ id: "navigation", name: "네비게이션", icon: Navigation, description: "메뉴, 탭, 아코디언 등" },
|
||
|
|
{ id: "business", name: "비즈니스", icon: Building, description: "대시보드, 차트, 리포트 등" },
|
||
|
|
{ id: "form", name: "폼", icon: FileText, description: "입력 폼, 설정 패널 등" },
|
||
|
|
{ id: "table", name: "테이블", icon: Table, description: "데이터 테이블, 목록 등" },
|
||
|
|
{ id: "dashboard", name: "대시보드", icon: LayoutDashboard, description: "위젯, 카드 레이아웃 등" },
|
||
|
|
] as const;
|
||
|
|
|
||
|
|
// 레이아웃 템플릿 정의
|
||
|
|
const LAYOUT_TEMPLATES = [
|
||
|
|
{
|
||
|
|
id: "2-column",
|
||
|
|
name: "2열 레이아웃",
|
||
|
|
description: "좌우 2개 영역으로 구성",
|
||
|
|
zones: 2,
|
||
|
|
example: "사이드바 + 메인 콘텐츠",
|
||
|
|
icon: "▢ ▢",
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: "3-column",
|
||
|
|
name: "3열 레이아웃",
|
||
|
|
description: "좌측, 중앙, 우측 3개 영역",
|
||
|
|
zones: 3,
|
||
|
|
example: "네비 + 콘텐츠 + 사이드",
|
||
|
|
icon: "▢ ▢ ▢",
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: "header-content",
|
||
|
|
name: "헤더-콘텐츠",
|
||
|
|
description: "상단 헤더 + 하단 콘텐츠",
|
||
|
|
zones: 2,
|
||
|
|
example: "제목 + 내용 영역",
|
||
|
|
icon: "▬\n▢",
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: "card-grid",
|
||
|
|
name: "카드 그리드",
|
||
|
|
description: "2x2 카드 격자 구조",
|
||
|
|
zones: 4,
|
||
|
|
example: "대시보드, 통계 패널",
|
||
|
|
icon: "▢▢\n▢▢",
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: "accordion",
|
||
|
|
name: "아코디언",
|
||
|
|
description: "접고 펼칠 수 있는 섹션들",
|
||
|
|
zones: 3,
|
||
|
|
example: "FAQ, 설정 패널",
|
||
|
|
icon: "▷ ▽ ▷",
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: "tabs",
|
||
|
|
name: "탭 레이아웃",
|
||
|
|
description: "탭으로 구성된 다중 패널",
|
||
|
|
zones: 3,
|
||
|
|
example: "설정, 상세 정보",
|
||
|
|
icon: "[Tab1][Tab2][Tab3]",
|
||
|
|
},
|
||
|
|
];
|
||
|
|
|
||
|
|
export const LayoutFormModal: React.FC<LayoutFormModalProps> = ({ open, onOpenChange, onSuccess }) => {
|
||
|
|
const [step, setStep] = useState<"basic" | "template" | "advanced">("basic");
|
||
|
|
const [formData, setFormData] = useState({
|
||
|
|
name: "",
|
||
|
|
nameEng: "",
|
||
|
|
description: "",
|
||
|
|
category: "" as LayoutCategory | "",
|
||
|
|
zones: 2,
|
||
|
|
template: "",
|
||
|
|
author: "Developer",
|
||
|
|
});
|
||
|
|
const [isGenerating, setIsGenerating] = useState(false);
|
||
|
|
const [generationResult, setGenerationResult] = useState<{
|
||
|
|
success: boolean;
|
||
|
|
message: string;
|
||
|
|
files?: string[];
|
||
|
|
} | null>(null);
|
||
|
|
|
||
|
|
const handleReset = () => {
|
||
|
|
setStep("basic");
|
||
|
|
setFormData({
|
||
|
|
name: "",
|
||
|
|
nameEng: "",
|
||
|
|
description: "",
|
||
|
|
category: "",
|
||
|
|
zones: 2,
|
||
|
|
template: "",
|
||
|
|
author: "Developer",
|
||
|
|
});
|
||
|
|
setGenerationResult(null);
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleClose = () => {
|
||
|
|
handleReset();
|
||
|
|
onOpenChange(false);
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleNext = () => {
|
||
|
|
if (step === "basic") {
|
||
|
|
setStep("template");
|
||
|
|
} else if (step === "template") {
|
||
|
|
setStep("advanced");
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleBack = () => {
|
||
|
|
if (step === "template") {
|
||
|
|
setStep("basic");
|
||
|
|
} else if (step === "advanced") {
|
||
|
|
setStep("template");
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const validateBasic = () => {
|
||
|
|
return formData.name.trim() && formData.category && formData.description.trim();
|
||
|
|
};
|
||
|
|
|
||
|
|
const validateTemplate = () => {
|
||
|
|
return formData.template && formData.zones > 0;
|
||
|
|
};
|
||
|
|
|
||
|
|
const generateLayout = async () => {
|
||
|
|
try {
|
||
|
|
setIsGenerating(true);
|
||
|
|
|
||
|
|
// CLI 명령어 구성
|
||
|
|
const command = [
|
||
|
|
formData.name.toLowerCase().replace(/[^a-z0-9-]/g, "-"),
|
||
|
|
`--category=${formData.category}`,
|
||
|
|
`--zones=${formData.zones}`,
|
||
|
|
`--description="${formData.description}"`,
|
||
|
|
formData.author !== "Developer" ? `--author="${formData.author}"` : null,
|
||
|
|
]
|
||
|
|
.filter(Boolean)
|
||
|
|
.join(" ");
|
||
|
|
|
||
|
|
// API 호출로 CLI 명령어 실행
|
||
|
|
const response = await fetch("/api/admin/layouts/generate", {
|
||
|
|
method: "POST",
|
||
|
|
headers: {
|
||
|
|
"Content-Type": "application/json",
|
||
|
|
},
|
||
|
|
body: JSON.stringify({
|
||
|
|
command,
|
||
|
|
layoutData: formData,
|
||
|
|
}),
|
||
|
|
});
|
||
|
|
|
||
|
|
const result = await response.json();
|
||
|
|
|
||
|
|
if (result.success) {
|
||
|
|
setGenerationResult({
|
||
|
|
success: true,
|
||
|
|
message: "레이아웃이 성공적으로 생성되었습니다!",
|
||
|
|
files: result.files || [],
|
||
|
|
});
|
||
|
|
|
||
|
|
toast.success("레이아웃 생성 완료");
|
||
|
|
|
||
|
|
// 3초 후 자동으로 모달 닫고 새로고침
|
||
|
|
setTimeout(() => {
|
||
|
|
handleClose();
|
||
|
|
onSuccess();
|
||
|
|
}, 3000);
|
||
|
|
} else {
|
||
|
|
setGenerationResult({
|
||
|
|
success: false,
|
||
|
|
message: result.message || "레이아웃 생성에 실패했습니다.",
|
||
|
|
});
|
||
|
|
toast.error("레이아웃 생성 실패");
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error("레이아웃 생성 오류:", error);
|
||
|
|
setGenerationResult({
|
||
|
|
success: false,
|
||
|
|
message: "서버 오류가 발생했습니다.",
|
||
|
|
});
|
||
|
|
toast.error("서버 오류");
|
||
|
|
} finally {
|
||
|
|
setIsGenerating(false);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
return (
|
||
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||
|
|
<DialogContent className="max-h-[90vh] max-w-4xl overflow-y-auto">
|
||
|
|
<DialogHeader>
|
||
|
|
<DialogTitle className="flex items-center gap-2">
|
||
|
|
<Wand2 className="h-5 w-5" />새 레이아웃 생성
|
||
|
|
</DialogTitle>
|
||
|
|
<DialogDescription>GUI를 통해 새로운 레이아웃을 쉽게 생성할 수 있습니다.</DialogDescription>
|
||
|
|
</DialogHeader>
|
||
|
|
|
||
|
|
{/* 단계 표시기 */}
|
||
|
|
<div className="mb-6 flex items-center justify-center">
|
||
|
|
<div className="flex items-center gap-4">
|
||
|
|
<div
|
||
|
|
className={`flex items-center gap-2 ${step === "basic" ? "text-blue-600" : step === "template" || step === "advanced" ? "text-green-600" : "text-gray-400"}`}
|
||
|
|
>
|
||
|
|
<div
|
||
|
|
className={`flex h-8 w-8 items-center justify-center rounded-full text-sm font-medium ${step === "basic" ? "bg-blue-100 text-blue-600" : step === "template" || step === "advanced" ? "bg-green-100 text-green-600" : "bg-gray-100"}`}
|
||
|
|
>
|
||
|
|
1
|
||
|
|
</div>
|
||
|
|
<span className="text-sm font-medium">기본 정보</span>
|
||
|
|
</div>
|
||
|
|
<div className="h-px w-8 bg-gray-300" />
|
||
|
|
<div
|
||
|
|
className={`flex items-center gap-2 ${step === "template" ? "text-blue-600" : step === "advanced" ? "text-green-600" : "text-gray-400"}`}
|
||
|
|
>
|
||
|
|
<div
|
||
|
|
className={`flex h-8 w-8 items-center justify-center rounded-full text-sm font-medium ${step === "template" ? "bg-blue-100 text-blue-600" : step === "advanced" ? "bg-green-100 text-green-600" : "bg-gray-100"}`}
|
||
|
|
>
|
||
|
|
2
|
||
|
|
</div>
|
||
|
|
<span className="text-sm font-medium">템플릿 선택</span>
|
||
|
|
</div>
|
||
|
|
<div className="h-px w-8 bg-gray-300" />
|
||
|
|
<div className={`flex items-center gap-2 ${step === "advanced" ? "text-blue-600" : "text-gray-400"}`}>
|
||
|
|
<div
|
||
|
|
className={`flex h-8 w-8 items-center justify-center rounded-full text-sm font-medium ${step === "advanced" ? "bg-blue-100 text-blue-600" : "bg-gray-100"}`}
|
||
|
|
>
|
||
|
|
3
|
||
|
|
</div>
|
||
|
|
<span className="text-sm font-medium">고급 설정</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* 단계별 컨텐츠 */}
|
||
|
|
<div className="space-y-6">
|
||
|
|
{step === "basic" && (
|
||
|
|
<div className="space-y-4">
|
||
|
|
<div className="grid grid-cols-2 gap-4">
|
||
|
|
<div className="space-y-2">
|
||
|
|
<Label htmlFor="name">레이아웃 이름 *</Label>
|
||
|
|
<Input
|
||
|
|
id="name"
|
||
|
|
placeholder="예: 사이드바, 대시보드, 카드그리드"
|
||
|
|
value={formData.name}
|
||
|
|
onChange={(e) => setFormData((prev) => ({ ...prev, name: e.target.value }))}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
<div className="space-y-2">
|
||
|
|
<Label htmlFor="nameEng">영문 이름</Label>
|
||
|
|
<Input
|
||
|
|
id="nameEng"
|
||
|
|
placeholder="예: Sidebar, Dashboard, CardGrid"
|
||
|
|
value={formData.nameEng}
|
||
|
|
onChange={(e) => setFormData((prev) => ({ ...prev, nameEng: e.target.value }))}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="space-y-2">
|
||
|
|
<Label>카테고리 *</Label>
|
||
|
|
<div className="grid grid-cols-2 gap-3">
|
||
|
|
{CATEGORIES.map((category) => {
|
||
|
|
const IconComponent = category.icon;
|
||
|
|
return (
|
||
|
|
<Card
|
||
|
|
key={category.id}
|
||
|
|
className={`cursor-pointer transition-all ${
|
||
|
|
formData.category === category.id ? "bg-blue-50 ring-2 ring-blue-500" : "hover:bg-gray-50"
|
||
|
|
}`}
|
||
|
|
onClick={() => setFormData((prev) => ({ ...prev, category: category.id }))}
|
||
|
|
>
|
||
|
|
<CardContent className="p-4">
|
||
|
|
<div className="flex items-center gap-3">
|
||
|
|
<IconComponent className="h-5 w-5 text-gray-600" />
|
||
|
|
<div>
|
||
|
|
<div className="font-medium">{category.name}</div>
|
||
|
|
<div className="text-xs text-gray-500">{category.description}</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
);
|
||
|
|
})}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="space-y-2">
|
||
|
|
<Label htmlFor="description">설명 *</Label>
|
||
|
|
<Textarea
|
||
|
|
id="description"
|
||
|
|
placeholder="레이아웃의 용도와 특징을 설명해주세요..."
|
||
|
|
value={formData.description}
|
||
|
|
onChange={(e) => setFormData((prev) => ({ ...prev, description: e.target.value }))}
|
||
|
|
rows={3}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{step === "template" && (
|
||
|
|
<div className="space-y-4">
|
||
|
|
<div>
|
||
|
|
<Label>레이아웃 템플릿 *</Label>
|
||
|
|
<p className="mb-3 text-sm text-gray-500">원하는 레이아웃 구조를 선택하세요</p>
|
||
|
|
<div className="grid grid-cols-2 gap-3">
|
||
|
|
{LAYOUT_TEMPLATES.map((template) => (
|
||
|
|
<Card
|
||
|
|
key={template.id}
|
||
|
|
className={`cursor-pointer transition-all ${
|
||
|
|
formData.template === template.id ? "bg-blue-50 ring-2 ring-blue-500" : "hover:bg-gray-50"
|
||
|
|
}`}
|
||
|
|
onClick={() =>
|
||
|
|
setFormData((prev) => ({
|
||
|
|
...prev,
|
||
|
|
template: template.id,
|
||
|
|
zones: template.zones,
|
||
|
|
}))
|
||
|
|
}
|
||
|
|
>
|
||
|
|
<CardContent className="p-4">
|
||
|
|
<div className="space-y-2">
|
||
|
|
<div className="flex items-center justify-between">
|
||
|
|
<div className="font-medium">{template.name}</div>
|
||
|
|
<Badge variant="secondary">{template.zones}개 영역</Badge>
|
||
|
|
</div>
|
||
|
|
<div className="text-sm text-gray-600">{template.description}</div>
|
||
|
|
<div className="text-xs text-gray-500">예: {template.example}</div>
|
||
|
|
<div className="rounded bg-gray-100 p-2 text-center font-mono text-xs">{template.icon}</div>
|
||
|
|
</div>
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="space-y-2">
|
||
|
|
<Label htmlFor="zones">영역 개수</Label>
|
||
|
|
<div className="flex items-center gap-4">
|
||
|
|
<Button
|
||
|
|
type="button"
|
||
|
|
variant="outline"
|
||
|
|
size="sm"
|
||
|
|
onClick={() =>
|
||
|
|
setFormData((prev) => ({
|
||
|
|
...prev,
|
||
|
|
zones: Math.max(1, prev.zones - 1),
|
||
|
|
}))
|
||
|
|
}
|
||
|
|
disabled={formData.zones <= 1}
|
||
|
|
>
|
||
|
|
<Minus className="h-4 w-4" />
|
||
|
|
</Button>
|
||
|
|
<Input
|
||
|
|
id="zones"
|
||
|
|
type="number"
|
||
|
|
min="1"
|
||
|
|
max="10"
|
||
|
|
value={formData.zones}
|
||
|
|
onChange={(e) =>
|
||
|
|
setFormData((prev) => ({
|
||
|
|
...prev,
|
||
|
|
zones: parseInt(e.target.value) || 1,
|
||
|
|
}))
|
||
|
|
}
|
||
|
|
className="w-20 text-center"
|
||
|
|
/>
|
||
|
|
<Button
|
||
|
|
type="button"
|
||
|
|
variant="outline"
|
||
|
|
size="sm"
|
||
|
|
onClick={() =>
|
||
|
|
setFormData((prev) => ({
|
||
|
|
...prev,
|
||
|
|
zones: Math.min(10, prev.zones + 1),
|
||
|
|
}))
|
||
|
|
}
|
||
|
|
disabled={formData.zones >= 10}
|
||
|
|
>
|
||
|
|
<Plus className="h-4 w-4" />
|
||
|
|
</Button>
|
||
|
|
<span className="text-sm text-gray-500">개 영역</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{step === "advanced" && (
|
||
|
|
<div className="space-y-4">
|
||
|
|
{generationResult ? (
|
||
|
|
<Alert
|
||
|
|
className={generationResult.success ? "border-green-200 bg-green-50" : "border-red-200 bg-red-50"}
|
||
|
|
>
|
||
|
|
<Info className="h-4 w-4" />
|
||
|
|
<AlertDescription className={generationResult.success ? "text-green-800" : "text-red-800"}>
|
||
|
|
{generationResult.message}
|
||
|
|
{generationResult.success && generationResult.files && (
|
||
|
|
<div className="mt-2">
|
||
|
|
<div className="text-sm font-medium">생성된 파일들:</div>
|
||
|
|
<ul className="mt-1 space-y-1 text-xs">
|
||
|
|
{generationResult.files.map((file, index) => (
|
||
|
|
<li key={index} className="text-green-700">
|
||
|
|
• {file}
|
||
|
|
</li>
|
||
|
|
))}
|
||
|
|
</ul>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</AlertDescription>
|
||
|
|
</Alert>
|
||
|
|
) : (
|
||
|
|
<>
|
||
|
|
<div className="space-y-2">
|
||
|
|
<Label htmlFor="author">작성자</Label>
|
||
|
|
<Input
|
||
|
|
id="author"
|
||
|
|
value={formData.author}
|
||
|
|
onChange={(e) => setFormData((prev) => ({ ...prev, author: e.target.value }))}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<Card>
|
||
|
|
<CardHeader>
|
||
|
|
<CardTitle className="text-sm">생성 미리보기</CardTitle>
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent className="space-y-2 text-sm">
|
||
|
|
<div>
|
||
|
|
<strong>이름:</strong> {formData.name || "이름 없음"}
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<strong>카테고리:</strong>{" "}
|
||
|
|
{CATEGORIES.find((c) => c.id === formData.category)?.name || "선택 안됨"}
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<strong>템플릿:</strong>{" "}
|
||
|
|
{LAYOUT_TEMPLATES.find((t) => t.id === formData.template)?.name || "선택 안됨"}
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<strong>영역 개수:</strong> {formData.zones}개
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<strong>생성될 파일:</strong>
|
||
|
|
</div>
|
||
|
|
<ul className="ml-4 space-y-1 text-xs text-gray-600">
|
||
|
|
<li>• {formData.name.toLowerCase()}/index.ts</li>
|
||
|
|
<li>
|
||
|
|
• {formData.name.toLowerCase()}/{formData.name}Layout.tsx
|
||
|
|
</li>
|
||
|
|
<li>
|
||
|
|
• {formData.name.toLowerCase()}/{formData.name}LayoutRenderer.tsx
|
||
|
|
</li>
|
||
|
|
<li>• {formData.name.toLowerCase()}/config.ts</li>
|
||
|
|
<li>• {formData.name.toLowerCase()}/types.ts</li>
|
||
|
|
<li>• {formData.name.toLowerCase()}/README.md</li>
|
||
|
|
</ul>
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
</>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<DialogFooter className="gap-2">
|
||
|
|
{step !== "basic" && !generationResult && (
|
||
|
|
<Button variant="outline" onClick={handleBack}>
|
||
|
|
이전
|
||
|
|
</Button>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{step === "basic" && (
|
||
|
|
<Button onClick={handleNext} disabled={!validateBasic()}>
|
||
|
|
다음
|
||
|
|
</Button>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{step === "template" && (
|
||
|
|
<Button onClick={handleNext} disabled={!validateTemplate()}>
|
||
|
|
다음
|
||
|
|
</Button>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{step === "advanced" && !generationResult && (
|
||
|
|
<Button onClick={generateLayout} disabled={isGenerating}>
|
||
|
|
{isGenerating ? "생성 중..." : "레이아웃 생성"}
|
||
|
|
</Button>
|
||
|
|
)}
|
||
|
|
|
||
|
|
<Button variant="outline" onClick={handleClose}>
|
||
|
|
{generationResult?.success ? "완료" : "취소"}
|
||
|
|
</Button>
|
||
|
|
</DialogFooter>
|
||
|
|
</DialogContent>
|
||
|
|
</Dialog>
|
||
|
|
);
|
||
|
|
};
|