467 lines
17 KiB
TypeScript
467 lines
17 KiB
TypeScript
"use client";
|
|
|
|
import React, { useState } from "react";
|
|
import { useRouter } from "next/navigation";
|
|
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 { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Switch } from "@/components/ui/switch";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { toast } from "sonner";
|
|
import { ArrowLeft, Save, RotateCcw } from "lucide-react";
|
|
import { useButtonActions, type ButtonActionFormData } from "@/hooks/admin/useButtonActions";
|
|
import Link from "next/link";
|
|
|
|
// 기본 카테고리 목록
|
|
const DEFAULT_CATEGORIES = ["crud", "navigation", "utility", "custom"];
|
|
|
|
// 기본 변형 목록
|
|
const DEFAULT_VARIANTS = ["default", "destructive", "outline", "secondary", "ghost", "link"];
|
|
|
|
export default function NewButtonActionPage() {
|
|
const router = useRouter();
|
|
const { createButtonAction, isCreating, createError } = useButtonActions();
|
|
|
|
const [formData, setFormData] = useState<ButtonActionFormData>({
|
|
action_type: "",
|
|
action_name: "",
|
|
action_name_eng: "",
|
|
description: "",
|
|
category: "general",
|
|
default_text: "",
|
|
default_text_eng: "",
|
|
default_icon: "",
|
|
default_color: "",
|
|
default_variant: "default",
|
|
confirmation_required: false,
|
|
confirmation_message: "",
|
|
validation_rules: {},
|
|
action_config: {},
|
|
sort_order: 0,
|
|
is_active: "Y",
|
|
});
|
|
|
|
const [jsonErrors, setJsonErrors] = useState<{
|
|
validation_rules?: string;
|
|
action_config?: string;
|
|
}>({});
|
|
|
|
// JSON 문자열 상태 (편집용)
|
|
const [jsonStrings, setJsonStrings] = useState({
|
|
validation_rules: "{}",
|
|
action_config: "{}",
|
|
});
|
|
|
|
// 입력값 변경 핸들러
|
|
const handleInputChange = (field: keyof ButtonActionFormData, value: any) => {
|
|
setFormData((prev) => ({
|
|
...prev,
|
|
[field]: value,
|
|
}));
|
|
};
|
|
|
|
// JSON 입력 변경 핸들러
|
|
const handleJsonChange = (field: "validation_rules" | "action_config", value: string) => {
|
|
setJsonStrings((prev) => ({
|
|
...prev,
|
|
[field]: value,
|
|
}));
|
|
|
|
// JSON 파싱 시도
|
|
try {
|
|
const parsed = value.trim() ? JSON.parse(value) : {};
|
|
setFormData((prev) => ({
|
|
...prev,
|
|
[field]: parsed,
|
|
}));
|
|
setJsonErrors((prev) => ({
|
|
...prev,
|
|
[field]: undefined,
|
|
}));
|
|
} catch (error) {
|
|
setJsonErrors((prev) => ({
|
|
...prev,
|
|
[field]: "유효하지 않은 JSON 형식입니다.",
|
|
}));
|
|
}
|
|
};
|
|
|
|
// 폼 유효성 검사
|
|
const validateForm = (): boolean => {
|
|
if (!formData.action_type.trim()) {
|
|
toast.error("액션 타입을 입력해주세요.");
|
|
return false;
|
|
}
|
|
|
|
if (!formData.action_name.trim()) {
|
|
toast.error("액션명을 입력해주세요.");
|
|
return false;
|
|
}
|
|
|
|
if (!formData.category.trim()) {
|
|
toast.error("카테고리를 선택해주세요.");
|
|
return false;
|
|
}
|
|
|
|
// JSON 에러가 있는지 확인
|
|
const hasJsonErrors = Object.values(jsonErrors).some((error) => error);
|
|
if (hasJsonErrors) {
|
|
toast.error("JSON 형식 오류를 수정해주세요.");
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
// 저장 핸들러
|
|
const handleSave = async () => {
|
|
if (!validateForm()) return;
|
|
|
|
try {
|
|
await createButtonAction(formData);
|
|
toast.success("버튼 액션이 성공적으로 생성되었습니다.");
|
|
router.push("/admin/system-settings/button-actions");
|
|
} catch (error) {
|
|
toast.error(error instanceof Error ? error.message : "생성 중 오류가 발생했습니다.");
|
|
}
|
|
};
|
|
|
|
// 폼 초기화
|
|
const handleReset = () => {
|
|
setFormData({
|
|
action_type: "",
|
|
action_name: "",
|
|
action_name_eng: "",
|
|
description: "",
|
|
category: "general",
|
|
default_text: "",
|
|
default_text_eng: "",
|
|
default_icon: "",
|
|
default_color: "",
|
|
default_variant: "default",
|
|
confirmation_required: false,
|
|
confirmation_message: "",
|
|
validation_rules: {},
|
|
action_config: {},
|
|
sort_order: 0,
|
|
is_active: "Y",
|
|
});
|
|
setJsonStrings({
|
|
validation_rules: "{}",
|
|
action_config: "{}",
|
|
});
|
|
setJsonErrors({});
|
|
};
|
|
|
|
return (
|
|
<div className="container mx-auto px-4 py-6">
|
|
{/* 헤더 */}
|
|
<div className="mb-6 flex items-center gap-4">
|
|
<Link href="/admin/system-settings/button-actions">
|
|
<Button variant="ghost" size="sm">
|
|
<ArrowLeft className="mr-2 h-4 w-4" />
|
|
목록으로
|
|
</Button>
|
|
</Link>
|
|
<div>
|
|
<h1 className="text-3xl font-bold tracking-tight">새 버튼 액션 추가</h1>
|
|
<p className="text-muted-foreground">새로운 버튼 액션을 생성하여 화면관리에서 사용할 수 있습니다.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
|
|
{/* 기본 정보 */}
|
|
<Card className="lg:col-span-2">
|
|
<CardHeader>
|
|
<CardTitle>기본 정보</CardTitle>
|
|
<CardDescription>버튼 액션의 기본적인 정보를 입력해주세요.</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-6">
|
|
{/* 액션 타입 */}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="action_type">
|
|
액션 타입 <span className="text-red-500">*</span>
|
|
</Label>
|
|
<Input
|
|
id="action_type"
|
|
value={formData.action_type}
|
|
onChange={(e) => handleInputChange("action_type", e.target.value)}
|
|
placeholder="예: save, delete, edit..."
|
|
className="font-mono"
|
|
/>
|
|
<p className="text-muted-foreground text-xs">영문 소문자, 숫자, 언더스코어(_)만 사용 가능합니다.</p>
|
|
</div>
|
|
|
|
{/* 액션명 */}
|
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="action_name">
|
|
액션명 <span className="text-red-500">*</span>
|
|
</Label>
|
|
<Input
|
|
id="action_name"
|
|
value={formData.action_name}
|
|
onChange={(e) => handleInputChange("action_name", e.target.value)}
|
|
placeholder="예: 저장"
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="action_name_eng">영문명</Label>
|
|
<Input
|
|
id="action_name_eng"
|
|
value={formData.action_name_eng}
|
|
onChange={(e) => handleInputChange("action_name_eng", e.target.value)}
|
|
placeholder="예: Save"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 카테고리 */}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="category">
|
|
카테고리 <span className="text-red-500">*</span>
|
|
</Label>
|
|
<Select value={formData.category} onValueChange={(value) => handleInputChange("category", value)}>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="카테고리 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{DEFAULT_CATEGORIES.map((category) => (
|
|
<SelectItem key={category} value={category}>
|
|
{category}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{/* 설명 */}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="description">설명</Label>
|
|
<Textarea
|
|
id="description"
|
|
value={formData.description}
|
|
onChange={(e) => handleInputChange("description", e.target.value)}
|
|
placeholder="버튼 액션에 대한 설명을 입력해주세요..."
|
|
rows={3}
|
|
/>
|
|
</div>
|
|
|
|
{/* 정렬 순서 */}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="sort_order">정렬 순서</Label>
|
|
<Input
|
|
id="sort_order"
|
|
type="number"
|
|
value={formData.sort_order}
|
|
onChange={(e) => handleInputChange("sort_order", parseInt(e.target.value) || 0)}
|
|
placeholder="0"
|
|
min="0"
|
|
/>
|
|
<p className="text-muted-foreground text-xs">낮은 숫자일수록 먼저 표시됩니다.</p>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* 상태 설정 */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>상태 설정</CardTitle>
|
|
<CardDescription>버튼 액션의 활성화 상태를 설정합니다.</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="flex items-center justify-between">
|
|
<div className="space-y-1">
|
|
<Label htmlFor="is_active">활성화 상태</Label>
|
|
<p className="text-muted-foreground text-xs">비활성화 시 화면관리에서 사용할 수 없습니다.</p>
|
|
</div>
|
|
<Switch
|
|
id="is_active"
|
|
checked={formData.is_active === "Y"}
|
|
onCheckedChange={(checked) => handleInputChange("is_active", checked ? "Y" : "N")}
|
|
/>
|
|
</div>
|
|
<div className="mt-4">
|
|
<Badge variant={formData.is_active === "Y" ? "default" : "secondary"}>
|
|
{formData.is_active === "Y" ? "활성화" : "비활성화"}
|
|
</Badge>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* 기본 설정 */}
|
|
<Card className="lg:col-span-3">
|
|
<CardHeader>
|
|
<CardTitle>기본 설정</CardTitle>
|
|
<CardDescription>버튼의 기본 스타일과 동작을 설정합니다.</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
|
|
{/* 기본 텍스트 */}
|
|
<div className="grid grid-cols-1 gap-4">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="default_text">기본 텍스트</Label>
|
|
<Input
|
|
id="default_text"
|
|
value={formData.default_text}
|
|
onChange={(e) => handleInputChange("default_text", e.target.value)}
|
|
placeholder="예: 저장"
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="default_text_eng">영문 텍스트</Label>
|
|
<Input
|
|
id="default_text_eng"
|
|
value={formData.default_text_eng}
|
|
onChange={(e) => handleInputChange("default_text_eng", e.target.value)}
|
|
placeholder="예: Save"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 아이콘 및 색상 */}
|
|
<div className="grid grid-cols-1 gap-4">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="default_icon">기본 아이콘</Label>
|
|
<Input
|
|
id="default_icon"
|
|
value={formData.default_icon}
|
|
onChange={(e) => handleInputChange("default_icon", e.target.value)}
|
|
placeholder="예: Save (Lucide 아이콘명)"
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="default_color">기본 색상</Label>
|
|
<Input
|
|
id="default_color"
|
|
value={formData.default_color}
|
|
onChange={(e) => handleInputChange("default_color", e.target.value)}
|
|
placeholder="예: blue, red, green..."
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 변형 */}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="default_variant">기본 변형</Label>
|
|
<Select
|
|
value={formData.default_variant}
|
|
onValueChange={(value) => handleInputChange("default_variant", value)}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="변형 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{DEFAULT_VARIANTS.map((variant) => (
|
|
<SelectItem key={variant} value={variant}>
|
|
{variant}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* 확인 설정 */}
|
|
<Card className="lg:col-span-3">
|
|
<CardHeader>
|
|
<CardTitle>확인 설정</CardTitle>
|
|
<CardDescription>버튼 실행 전 확인 메시지 설정입니다.</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
<div className="flex items-center justify-between">
|
|
<div className="space-y-1">
|
|
<Label htmlFor="confirmation_required">확인 메시지 필요</Label>
|
|
<p className="text-muted-foreground text-xs">버튼 실행 전 사용자 확인을 받습니다.</p>
|
|
</div>
|
|
<Switch
|
|
id="confirmation_required"
|
|
checked={formData.confirmation_required}
|
|
onCheckedChange={(checked) => handleInputChange("confirmation_required", checked)}
|
|
/>
|
|
</div>
|
|
|
|
{formData.confirmation_required && (
|
|
<div className="space-y-2">
|
|
<Label htmlFor="confirmation_message">확인 메시지</Label>
|
|
<Textarea
|
|
id="confirmation_message"
|
|
value={formData.confirmation_message}
|
|
onChange={(e) => handleInputChange("confirmation_message", e.target.value)}
|
|
placeholder="예: 정말로 삭제하시겠습니까?"
|
|
rows={2}
|
|
/>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* JSON 설정 */}
|
|
<Card className="lg:col-span-3">
|
|
<CardHeader>
|
|
<CardTitle>고급 설정 (JSON)</CardTitle>
|
|
<CardDescription>버튼 액션의 세부 설정을 JSON 형식으로 입력할 수 있습니다.</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
|
|
{/* 유효성 검사 규칙 */}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="validation_rules">유효성 검사 규칙</Label>
|
|
<Textarea
|
|
id="validation_rules"
|
|
value={jsonStrings.validation_rules}
|
|
onChange={(e) => handleJsonChange("validation_rules", e.target.value)}
|
|
placeholder='{"requiresData": true, "minItems": 1}'
|
|
rows={4}
|
|
className="font-mono text-xs"
|
|
/>
|
|
{jsonErrors.validation_rules && <p className="text-xs text-red-500">{jsonErrors.validation_rules}</p>}
|
|
</div>
|
|
|
|
{/* 액션 설정 */}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="action_config">액션 설정</Label>
|
|
<Textarea
|
|
id="action_config"
|
|
value={jsonStrings.action_config}
|
|
onChange={(e) => handleJsonChange("action_config", e.target.value)}
|
|
placeholder='{"apiEndpoint": "/api/save", "redirectUrl": "/list"}'
|
|
rows={4}
|
|
className="font-mono text-xs"
|
|
/>
|
|
{jsonErrors.action_config && <p className="text-xs text-red-500">{jsonErrors.action_config}</p>}
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* 액션 버튼 */}
|
|
<div className="mt-6 flex justify-end gap-4">
|
|
<Button variant="outline" onClick={handleReset}>
|
|
<RotateCcw className="mr-2 h-4 w-4" />
|
|
초기화
|
|
</Button>
|
|
<Button onClick={handleSave} disabled={isCreating}>
|
|
<Save className="mr-2 h-4 w-4" />
|
|
{isCreating ? "생성 중..." : "저장"}
|
|
</Button>
|
|
</div>
|
|
|
|
{/* 에러 메시지 */}
|
|
{createError && (
|
|
<div className="mt-4 rounded-md border border-red-200 bg-red-50 p-4">
|
|
<p className="text-red-600">
|
|
생성 중 오류가 발생했습니다: {createError instanceof Error ? createError.message : "알 수 없는 오류"}
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|