461 lines
17 KiB
TypeScript
461 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 { useWebTypes, type WebTypeFormData } from "@/hooks/admin/useWebTypes";
|
|
import { AVAILABLE_COMPONENTS } from "@/lib/utils/availableComponents";
|
|
import { AVAILABLE_CONFIG_PANELS, getConfigPanelInfo } from "@/lib/utils/availableConfigPanels";
|
|
import Link from "next/link";
|
|
|
|
// 기본 카테고리 목록
|
|
const DEFAULT_CATEGORIES = ["input", "select", "display", "special"];
|
|
|
|
export default function NewWebTypePage() {
|
|
const router = useRouter();
|
|
const { createWebType, isCreating, createError } = useWebTypes();
|
|
|
|
const [formData, setFormData] = useState<WebTypeFormData>({
|
|
web_type: "",
|
|
type_name: "",
|
|
type_name_eng: "",
|
|
description: "",
|
|
category: "input",
|
|
component_name: "TextWidget",
|
|
config_panel: "none",
|
|
default_config: {},
|
|
validation_rules: {},
|
|
default_style: {},
|
|
input_properties: {},
|
|
sort_order: 0,
|
|
is_active: "Y",
|
|
});
|
|
|
|
const [jsonErrors, setJsonErrors] = useState<{
|
|
default_config?: string;
|
|
validation_rules?: string;
|
|
default_style?: string;
|
|
input_properties?: string;
|
|
}>({});
|
|
|
|
// JSON 문자열 상태 (편집용)
|
|
const [jsonStrings, setJsonStrings] = useState({
|
|
default_config: "{}",
|
|
validation_rules: "{}",
|
|
default_style: "{}",
|
|
input_properties: "{}",
|
|
});
|
|
|
|
// 입력값 변경 핸들러
|
|
const handleInputChange = (field: keyof WebTypeFormData, value: any) => {
|
|
setFormData((prev) => ({
|
|
...prev,
|
|
[field]: value,
|
|
}));
|
|
};
|
|
|
|
// JSON 입력 변경 핸들러
|
|
const handleJsonChange = (
|
|
field: "default_config" | "validation_rules" | "default_style" | "input_properties",
|
|
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.web_type.trim()) {
|
|
toast.error("웹타입 코드를 입력해주세요.");
|
|
return false;
|
|
}
|
|
|
|
if (!formData.type_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 createWebType(formData);
|
|
toast.success("웹타입이 성공적으로 생성되었습니다.");
|
|
router.push("/admin/standards");
|
|
} catch (error) {
|
|
toast.error(error instanceof Error ? error.message : "생성 중 오류가 발생했습니다.");
|
|
}
|
|
};
|
|
|
|
// 폼 초기화
|
|
const handleReset = () => {
|
|
setFormData({
|
|
web_type: "",
|
|
type_name: "",
|
|
type_name_eng: "",
|
|
description: "",
|
|
category: "input",
|
|
component_name: "TextWidget",
|
|
config_panel: "none",
|
|
default_config: {},
|
|
validation_rules: {},
|
|
default_style: {},
|
|
input_properties: {},
|
|
sort_order: 0,
|
|
is_active: "Y",
|
|
});
|
|
setJsonStrings({
|
|
default_config: "{}",
|
|
validation_rules: "{}",
|
|
default_style: "{}",
|
|
input_properties: "{}",
|
|
});
|
|
setJsonErrors({});
|
|
};
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gray-50">
|
|
<div className="container mx-auto p-6 space-y-6">
|
|
{/* 헤더 */}
|
|
<div className="mb-6 flex items-center gap-4">
|
|
<Link href="/admin/standards">
|
|
<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="web_type">
|
|
웹타입 코드 <span className="text-red-500">*</span>
|
|
</Label>
|
|
<Input
|
|
id="web_type"
|
|
value={formData.web_type}
|
|
onChange={(e) => handleInputChange("web_type", e.target.value)}
|
|
placeholder="예: text, number, email..."
|
|
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="type_name">
|
|
웹타입명 <span className="text-red-500">*</span>
|
|
</Label>
|
|
<Input
|
|
id="type_name"
|
|
value={formData.type_name}
|
|
onChange={(e) => handleInputChange("type_name", e.target.value)}
|
|
placeholder="예: 텍스트 입력"
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="type_name_eng">영문명</Label>
|
|
<Input
|
|
id="type_name_eng"
|
|
value={formData.type_name_eng}
|
|
onChange={(e) => handleInputChange("type_name_eng", e.target.value)}
|
|
placeholder="예: Text Input"
|
|
/>
|
|
</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="component_name">
|
|
연결된 컴포넌트 <span className="text-red-500">*</span>
|
|
</Label>
|
|
<Select
|
|
value={formData.component_name || "TextWidget"}
|
|
onValueChange={(value) => handleInputChange("component_name", value)}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="컴포넌트 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{AVAILABLE_COMPONENTS.map((component) => (
|
|
<SelectItem key={component.value} value={component.value}>
|
|
<div className="flex flex-col">
|
|
<span className="font-medium">{component.label}</span>
|
|
<span className="text-muted-foreground text-xs">{component.description}</span>
|
|
</div>
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
{formData.component_name && (
|
|
<div className="text-muted-foreground text-xs">
|
|
현재 선택:{" "}
|
|
<Badge variant="outline" className="font-mono">
|
|
{formData.component_name}
|
|
</Badge>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* 설정 패널 */}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="config_panel">설정 패널</Label>
|
|
<Select
|
|
value={formData.config_panel || "none"}
|
|
onValueChange={(value) => handleInputChange("config_panel", value === "none" ? null : value)}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="설정 패널 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{AVAILABLE_CONFIG_PANELS.map((panel) => (
|
|
<SelectItem key={panel.value} value={panel.value}>
|
|
<div className="flex flex-col">
|
|
<span className="font-medium">{panel.label}</span>
|
|
<span className="text-muted-foreground text-xs">{panel.description}</span>
|
|
</div>
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
{formData.config_panel && (
|
|
<div className="rounded-lg border border-green-200 bg-green-50 p-3">
|
|
<div className="flex items-center gap-2">
|
|
<div className="h-2 w-2 rounded-full bg-green-500"></div>
|
|
<span className="text-sm font-medium text-green-700">
|
|
현재 선택: {getConfigPanelInfo(formData.config_panel)?.label || formData.config_panel}
|
|
</span>
|
|
</div>
|
|
{getConfigPanelInfo(formData.config_panel)?.description && (
|
|
<p className="mt-1 ml-4 text-xs text-green-600">
|
|
{getConfigPanelInfo(formData.config_panel)?.description}
|
|
</p>
|
|
)}
|
|
</div>
|
|
)}
|
|
</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>
|
|
|
|
{/* 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="default_config">기본 설정</Label>
|
|
<Textarea
|
|
id="default_config"
|
|
value={jsonStrings.default_config}
|
|
onChange={(e) => handleJsonChange("default_config", e.target.value)}
|
|
placeholder='{"placeholder": "입력하세요..."}'
|
|
rows={4}
|
|
className="font-mono text-xs"
|
|
/>
|
|
{jsonErrors.default_config && <p className="text-xs text-red-500">{jsonErrors.default_config}</p>}
|
|
</div>
|
|
|
|
{/* 유효성 검사 규칙 */}
|
|
<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='{"required": true, "minLength": 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="default_style">기본 스타일</Label>
|
|
<Textarea
|
|
id="default_style"
|
|
value={jsonStrings.default_style}
|
|
onChange={(e) => handleJsonChange("default_style", e.target.value)}
|
|
placeholder='{"width": "100%", "height": "40px"}'
|
|
rows={4}
|
|
className="font-mono text-xs"
|
|
/>
|
|
{jsonErrors.default_style && <p className="text-xs text-red-500">{jsonErrors.default_style}</p>}
|
|
</div>
|
|
|
|
{/* 입력 속성 */}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="input_properties">HTML 입력 속성</Label>
|
|
<Textarea
|
|
id="input_properties"
|
|
value={jsonStrings.input_properties}
|
|
onChange={(e) => handleJsonChange("input_properties", e.target.value)}
|
|
placeholder='{"type": "text", "autoComplete": "off"}'
|
|
rows={4}
|
|
className="font-mono text-xs"
|
|
/>
|
|
{jsonErrors.input_properties && <p className="text-xs text-red-500">{jsonErrors.input_properties}</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>
|
|
</div>
|
|
);
|
|
}
|