"use client"; import React, { useState, useCallback, useEffect } from "react"; 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, ListOrdered } from "lucide-react"; import { toast } from "sonner"; import { showErrorToast } from "@/lib/utils/toastUtils"; import { NumberingRuleConfig, NumberingRulePart, SEPARATOR_OPTIONS, SeparatorType } from "@/types/numbering-rule"; import { CODE_PART_TYPE_OPTIONS } from "@/types/numbering-rule"; import { NumberingRuleCard } from "./NumberingRuleCard"; import { NumberingRulePreview, computePartDisplayItems, getPartTypeColorClass } from "./NumberingRulePreview"; import { getNumberingRules, saveNumberingRuleToTest } from "@/lib/api/numberingRule"; import { cn } from "@/lib/utils"; interface NumberingRuleDesignerProps { initialConfig?: NumberingRuleConfig; onSave?: (config: NumberingRuleConfig) => void; onChange?: (config: NumberingRuleConfig) => void; maxRules?: number; isPreview?: boolean; className?: string; currentTableName?: string; menuObjid?: number; } export const NumberingRuleDesigner: React.FC = ({ initialConfig, onSave, onChange, maxRules = 6, isPreview = false, className = "", currentTableName, menuObjid, }) => { const [rulesList, setRulesList] = useState([]); const [selectedRuleId, setSelectedRuleId] = useState(null); const [currentRule, setCurrentRule] = useState(null); const [selectedPartOrder, setSelectedPartOrder] = useState(null); const [loading, setLoading] = useState(false); const [separatorTypes, setSeparatorTypes] = useState>({}); const [customSeparators, setCustomSeparators] = useState>({}); const selectedRule = rulesList.find((r) => r.ruleId === selectedRuleId) ?? currentRule; // 좌측: 규칙 목록 로드 useEffect(() => { loadRules(); }, []); const loadRules = async () => { setLoading(true); try { const response = await getNumberingRules(); if (response.success && response.data) { setRulesList(response.data); if (response.data.length > 0 && !selectedRuleId) { const first = response.data[0]; setSelectedRuleId(first.ruleId); setCurrentRule(JSON.parse(JSON.stringify(first))); } } } catch (e) { console.error("채번 규칙 목록 로드 실패:", e); } finally { setLoading(false); } }; const handleSelectRule = (rule: NumberingRuleConfig) => { setSelectedRuleId(rule.ruleId); setCurrentRule(JSON.parse(JSON.stringify(rule))); setSelectedPartOrder(null); }; const handleAddNewRule = () => { const newRule: NumberingRuleConfig = { ruleId: `rule-${Date.now()}`, ruleName: "새 규칙", parts: [], separator: "-", resetPeriod: "none", currentSequence: 1, scopeType: "global", tableName: currentTableName ?? "", columnName: "", }; setRulesList((prev) => [...prev, newRule]); setSelectedRuleId(newRule.ruleId); setCurrentRule(JSON.parse(JSON.stringify(newRule))); setSelectedPartOrder(null); toast.success("새 규칙이 추가되었습니다"); }; useEffect(() => { if (currentRule) onChange?.(currentRule); }, [currentRule, onChange]); useEffect(() => { if (currentRule && currentRule.parts.length > 0) { const newSepTypes: Record = {}; const newCustomSeps: Record = {}; currentRule.parts.forEach((part) => { const sep = part.separatorAfter ?? currentRule.separator ?? "-"; if (sep === "") { newSepTypes[part.order] = "none"; newCustomSeps[part.order] = ""; } else { const opt = SEPARATOR_OPTIONS.find( (o) => o.value !== "custom" && o.value !== "none" && o.displayValue === sep ); if (opt) { newSepTypes[part.order] = opt.value; newCustomSeps[part.order] = ""; } else { newSepTypes[part.order] = "custom"; newCustomSeps[part.order] = sep; } } }); setSeparatorTypes(newSepTypes); setCustomSeparators(newCustomSeps); } }, [currentRule?.ruleId]); const handlePartSeparatorChange = useCallback((partOrder: number, type: SeparatorType) => { setSeparatorTypes((prev) => ({ ...prev, [partOrder]: type })); if (type !== "custom") { const option = SEPARATOR_OPTIONS.find((opt) => opt.value === type); const newSeparator = option?.displayValue ?? ""; setCustomSeparators((prev) => ({ ...prev, [partOrder]: "" })); setCurrentRule((prev) => { if (!prev) return null; return { ...prev, parts: prev.parts.map((p) => (p.order === partOrder ? { ...p, separatorAfter: newSeparator } : p)), }; }); } }, []); const handlePartCustomSeparatorChange = useCallback((partOrder: number, value: string) => { const trimmedValue = value.slice(0, 2); setCustomSeparators((prev) => ({ ...prev, [partOrder]: trimmedValue })); setCurrentRule((prev) => { if (!prev) return null; return { ...prev, parts: prev.parts.map((p) => (p.order === partOrder ? { ...p, separatorAfter: trimmedValue } : p)), }; }); }, []); 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" }, separatorAfter: "-", }; setCurrentRule((prev) => (prev ? { ...prev, parts: [...prev.parts, newPart] } : null)); setSeparatorTypes((prev) => ({ ...prev, [newPart.order]: "-" })); setCustomSeparators((prev) => ({ ...prev, [newPart.order]: "" })); toast.success(`규칙 ${newPart.order}가 추가되었습니다`); }, [currentRule, maxRules]); const handleUpdatePart = useCallback((partOrder: number, updates: Partial) => { setCurrentRule((prev) => { if (!prev) return null; return { ...prev, parts: prev.parts.map((p) => (p.order === partOrder ? { ...p, ...updates } : p)), }; }); }, []); const handleDeletePart = useCallback((partOrder: number) => { setCurrentRule((prev) => { if (!prev) return null; return { ...prev, parts: prev.parts .filter((p) => p.order !== partOrder) .map((p, i) => ({ ...p, order: i + 1 })), }; }); setSelectedPartOrder(null); toast.success("규칙이 삭제되었습니다"); }, []); const handleSave = useCallback(async () => { if (!currentRule) { toast.error("저장할 규칙이 없습니다"); return; } if (currentRule.parts.length === 0) { toast.error("최소 1개 이상의 규칙을 추가해주세요"); return; } setLoading(true); try { const defaultAutoConfigs: Record = { sequence: { sequenceLength: 3, startFrom: 1 }, number: { numberLength: 4, numberValue: 1 }, date: { dateFormat: "YYYYMMDD" }, text: { textValue: "" }, }; const partsWithDefaults = currentRule.parts.map((part) => { if (part.generationMethod === "auto") { const defaults = defaultAutoConfigs[part.partType] || {}; return { ...part, autoConfig: { ...defaults, ...part.autoConfig } }; } return part; }); const ruleToSave = { ...currentRule, parts: partsWithDefaults, scopeType: "global" as const, tableName: currentRule.tableName || currentTableName || "", columnName: currentRule.columnName || "", }; const response = await saveNumberingRuleToTest(ruleToSave); if (response.success && response.data) { const saved: NumberingRuleConfig = JSON.parse(JSON.stringify(response.data)); setCurrentRule(saved); setRulesList((prev) => { const idx = prev.findIndex((r) => r.ruleId === currentRule.ruleId); if (idx >= 0) { const next = [...prev]; next[idx] = saved; return next; } return [...prev, saved]; }); setSelectedRuleId(saved.ruleId); await onSave?.(response.data); toast.success("채번 규칙이 저장되었습니다"); } else { showErrorToast("채번 규칙 저장에 실패했습니다", response.error, { guidance: "설정을 확인하고 다시 시도해 주세요.", }); } } catch (error: unknown) { showErrorToast("채번 규칙 저장에 실패했습니다", error, { guidance: "설정을 확인하고 다시 시도해 주세요.", }); } finally { setLoading(false); } }, [currentRule, onSave, currentTableName]); const selectedPart = currentRule?.parts.find((p) => p.order === selectedPartOrder) ?? null; const globalSep = currentRule?.separator ?? "-"; const partItems = currentRule ? computePartDisplayItems(currentRule) : []; return (
{/* 좌측: 규칙 리스트 (code-nav, 220px) */}
채번 규칙 ({rulesList.length})
{loading && rulesList.length === 0 ? (
로딩 중...
) : rulesList.length === 0 ? (
규칙이 없습니다
) : ( rulesList.map((rule) => { const isSelected = selectedRuleId === rule.ruleId; return ( ); }) )}
{/* 우측: 미리보기 + 파이프라인 + 설정 + 저장 바 */}
{!currentRule ? (

규칙을 선택하세요

좌측에서 채번 규칙을 선택하거나 "추가"로 새 규칙을 만드세요

) : ( <>
setCurrentRule((prev) => (prev ? { ...prev, ruleName: e.target.value } : null))} placeholder="예: 프로젝트 코드" className="h-9 text-sm" />
{/* 큰 미리보기 스트립 (code-preview-strip) */}
{/* 파이프라인 영역 (code-pipeline-area) */}
코드 구성 {currentRule.parts.length}/{maxRules}
{currentRule.parts.length === 0 ? (
규칙을 추가하여 코드를 구성하세요
) : ( <> {currentRule.parts.map((part, index) => { const item = partItems.find((i) => i.order === part.order); const sep = part.separatorAfter ?? globalSep; const isSelected = selectedPartOrder === part.order; const typeLabel = CODE_PART_TYPE_OPTIONS.find((o) => o.value === part.partType)?.label ?? part.partType; return ( {index < currentRule.parts.length - 1 && ( {sep || "(없음)"} )} ); })} )}
{/* 설정 패널 (선택된 세그먼트 상세, code-config-panel) */} {selectedPart && (
handleUpdatePart(selectedPart.order, updates)} onDelete={() => handleDeletePart(selectedPart.order)} isPreview={isPreview} tableName={currentRule.tableName ?? currentTableName} />
{currentRule.parts.some((p) => p.order === selectedPart.order) && (
뒤 구분자 {separatorTypes[selectedPart.order] === "custom" && ( handlePartCustomSeparatorChange(selectedPart.order, e.target.value)} className="h-7 w-14 text-center text-[10px]" placeholder="2자" maxLength={2} /> )}
)}
)} {/* 저장 바 (code-save-bar) */}
{currentRule.tableName && ( 테이블: {currentRule.tableName} )} {currentRule.columnName && ( 컬럼: {currentRule.columnName} )} 구분자: {globalSep || "-"} {currentRule.resetPeriod && currentRule.resetPeriod !== "none" && ( 리셋: {currentRule.resetPeriod} )}
)}
); };