"use client"; import React, { useState, useCallback, 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 { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Plus, Save, Edit2, Trash2 } from "lucide-react"; import { toast } from "sonner"; import { NumberingRuleConfig, NumberingRulePart } from "@/types/numbering-rule"; import { NumberingRuleCard } from "./NumberingRuleCard"; import { NumberingRulePreview } from "./NumberingRulePreview"; import { getNumberingRules, createNumberingRule, updateNumberingRule, deleteNumberingRule, } from "@/lib/api/numberingRule"; interface NumberingRuleDesignerProps { initialConfig?: NumberingRuleConfig; onSave?: (config: NumberingRuleConfig) => void; onChange?: (config: NumberingRuleConfig) => void; maxRules?: number; isPreview?: boolean; className?: string; } export const NumberingRuleDesigner: React.FC = ({ initialConfig, onSave, onChange, maxRules = 6, isPreview = false, className = "", }) => { const [savedRules, setSavedRules] = useState([]); const [selectedRuleId, setSelectedRuleId] = useState(null); const [currentRule, setCurrentRule] = useState(null); const [loading, setLoading] = useState(false); const [leftTitle, setLeftTitle] = useState("저장된 규칙 목록"); const [rightTitle, setRightTitle] = useState("규칙 편집"); const [editingLeftTitle, setEditingLeftTitle] = useState(false); const [editingRightTitle, setEditingRightTitle] = useState(false); useEffect(() => { loadRules(); }, []); const loadRules = useCallback(async () => { setLoading(true); try { const response = await getNumberingRules(); if (response.success && response.data) { setSavedRules(response.data); } else { toast.error(response.error || "규칙 목록을 불러올 수 없습니다"); } } catch (error: any) { toast.error(`로딩 실패: ${error.message}`); } finally { setLoading(false); } }, []); useEffect(() => { if (currentRule) { onChange?.(currentRule); } }, [currentRule, onChange]); const handleAddPart = useCallback(() => { if (!currentRule) return; if (currentRule.parts.length >= maxRules) { toast.error(`최대 ${maxRules}개까지 추가할 수 있습니다`); return; } const newPart: NumberingRulePart = { id: `part-${Date.now()}`, order: currentRule.parts.length + 1, partType: "text", generationMethod: "auto", autoConfig: { textValue: "CODE" }, }; setCurrentRule((prev) => { if (!prev) return null; return { ...prev, parts: [...prev.parts, newPart] }; }); toast.success(`규칙 ${newPart.order}가 추가되었습니다`); }, [currentRule, maxRules]); const handleUpdatePart = useCallback((partId: string, updates: Partial) => { setCurrentRule((prev) => { if (!prev) return null; return { ...prev, parts: prev.parts.map((part) => (part.id === partId ? { ...part, ...updates } : part)), }; }); }, []); const handleDeletePart = useCallback((partId: string) => { setCurrentRule((prev) => { if (!prev) return null; return { ...prev, parts: prev.parts .filter((part) => part.id !== partId) .map((part, index) => ({ ...part, order: index + 1 })), }; }); toast.success("규칙이 삭제되었습니다"); }, []); const handleSave = useCallback(async () => { if (!currentRule) { toast.error("저장할 규칙이 없습니다"); return; } if (currentRule.parts.length === 0) { toast.error("최소 1개 이상의 규칙을 추가해주세요"); return; } setLoading(true); try { const existing = savedRules.find((r) => r.ruleId === currentRule.ruleId); let response; if (existing) { response = await updateNumberingRule(currentRule.ruleId, currentRule); } else { response = await createNumberingRule(currentRule); } if (response.success && response.data) { setSavedRules((prev) => { if (existing) { return prev.map((r) => (r.ruleId === currentRule.ruleId ? response.data! : r)); } else { return [...prev, response.data!]; } }); setCurrentRule(response.data); setSelectedRuleId(response.data.ruleId); await onSave?.(response.data); toast.success("채번 규칙이 저장되었습니다"); } else { toast.error(response.error || "저장 실패"); } } catch (error: any) { toast.error(`저장 실패: ${error.message}`); } finally { setLoading(false); } }, [currentRule, savedRules, onSave]); const handleSelectRule = useCallback((rule: NumberingRuleConfig) => { setSelectedRuleId(rule.ruleId); setCurrentRule(rule); toast.info(`"${rule.ruleName}" 규칙을 불러왔습니다`); }, []); const handleDeleteSavedRule = useCallback(async (ruleId: string) => { setLoading(true); try { const response = await deleteNumberingRule(ruleId); if (response.success) { setSavedRules((prev) => prev.filter((r) => r.ruleId !== ruleId)); if (selectedRuleId === ruleId) { setSelectedRuleId(null); setCurrentRule(null); } toast.success("규칙이 삭제되었습니다"); } else { toast.error(response.error || "삭제 실패"); } } catch (error: any) { toast.error(`삭제 실패: ${error.message}`); } finally { setLoading(false); } }, [selectedRuleId]); const handleNewRule = useCallback(() => { const newRule: NumberingRuleConfig = { ruleId: `rule-${Date.now()}`, ruleName: "새 채번 규칙", parts: [], separator: "-", resetPeriod: "none", currentSequence: 1, scopeType: "global", }; setSelectedRuleId(newRule.ruleId); setCurrentRule(newRule); toast.success("새 규칙이 생성되었습니다"); }, []); return (
{/* 좌측: 저장된 규칙 목록 */}
{editingLeftTitle ? ( setLeftTitle(e.target.value)} onBlur={() => setEditingLeftTitle(false)} onKeyDown={(e) => e.key === "Enter" && setEditingLeftTitle(false)} className="h-8 text-sm font-semibold" autoFocus /> ) : (

{leftTitle}

)}
{loading ? (

로딩 중...

) : savedRules.length === 0 ? (

저장된 규칙이 없습니다

) : ( savedRules.map((rule) => ( handleSelectRule(rule)} >
{rule.ruleName}

규칙 {rule.parts.length}개

)) )}
{/* 구분선 */}
{/* 우측: 편집 영역 */}
{!currentRule ? (

규칙을 선택해주세요

좌측에서 규칙을 선택하거나 새로 생성하세요

) : ( <>
{editingRightTitle ? ( setRightTitle(e.target.value)} onBlur={() => setEditingRightTitle(false)} onKeyDown={(e) => e.key === "Enter" && setEditingRightTitle(false)} className="h-8 text-sm font-semibold" autoFocus /> ) : (

{rightTitle}

)}
setCurrentRule((prev) => ({ ...prev!, ruleName: e.target.value })) } className="h-9" placeholder="예: 프로젝트 코드" />

{currentRule.scopeType === "menu" ? "이 규칙이 설정된 상위 메뉴의 모든 하위 메뉴에서 사용 가능합니다" : "회사 내 모든 메뉴에서 사용 가능한 전역 규칙입니다"}

미리보기

코드 구성

{currentRule.parts.length}/{maxRules}
{currentRule.parts.length === 0 ? (

규칙을 추가하여 코드를 구성하세요

) : (
{currentRule.parts.map((part) => ( handleUpdatePart(part.id, updates)} onDelete={() => handleDeletePart(part.id)} isPreview={isPreview} /> ))}
)}
)}
); };