"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, Search, Hash, Table2 } 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 { saveNumberingRuleToTest } from "@/lib/api/numberingRule"; import { apiClient } from "@/lib/api/client"; import { cn } from "@/lib/utils"; interface NumberingColumn { tableName: string; tableLabel: string; columnName: string; columnLabel: string; } interface GroupedColumns { tableLabel: string; columns: NumberingColumn[]; } 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 [numberingColumns, setNumberingColumns] = useState([]); const [selectedColumn, setSelectedColumn] = useState<{ tableName: string; columnName: string } | null>(null); const [currentRule, setCurrentRule] = useState(null); const [selectedPartOrder, setSelectedPartOrder] = useState(null); const [loading, setLoading] = useState(false); const [columnSearch, setColumnSearch] = useState(""); const [separatorTypes, setSeparatorTypes] = useState>({}); const [customSeparators, setCustomSeparators] = useState>({}); useEffect(() => { loadNumberingColumns(); }, []); const loadNumberingColumns = async () => { setLoading(true); try { const response = await apiClient.get("/table-management/numbering-columns"); if (response.data.success && response.data.data) { setNumberingColumns(response.data.data); } } catch (error: any) { console.error("채번 컬럼 목록 로드 실패:", error); } finally { setLoading(false); } }; const handleSelectColumn = async (tableName: string, columnName: string) => { setSelectedColumn({ tableName, columnName }); setSelectedPartOrder(null); setLoading(true); try { const response = await apiClient.get(`/numbering-rules/by-column/${tableName}/${columnName}`); if (response.data.success && response.data.data) { const rule = response.data.data as NumberingRuleConfig; setCurrentRule(JSON.parse(JSON.stringify(rule))); } else { const newRule: NumberingRuleConfig = { ruleId: `rule-${Date.now()}`, ruleName: `${columnName} 채번`, parts: [], separator: "-", resetPeriod: "none", currentSequence: 1, scopeType: "table", tableName, columnName, }; setCurrentRule(newRule); } } catch { const newRule: NumberingRuleConfig = { ruleId: `rule-${Date.now()}`, ruleName: `${columnName} 채번`, parts: [], separator: "-", resetPeriod: "none", currentSequence: 1, scopeType: "table", tableName, columnName, }; setCurrentRule(newRule); } finally { setLoading(false); } }; // 테이블별 그룹화 const groupedColumns = numberingColumns.reduce>((acc, col) => { if (!acc[col.tableName]) { acc[col.tableName] = { tableLabel: col.tableLabel, columns: [] }; } acc[col.tableName].columns.push(col); return acc; }, {}); // 검색 필터 const filteredGroups = Object.entries(groupedColumns).filter(([tableName, group]) => { if (!columnSearch) return true; const search = columnSearch.toLowerCase(); return ( tableName.toLowerCase().includes(search) || group.tableLabel.toLowerCase().includes(search) || group.columns.some( (c) => c.columnName.toLowerCase().includes(search) || c.columnLabel.toLowerCase().includes(search) ) ); }); 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: "table" as const, tableName: selectedColumn?.tableName || currentRule.tableName || "", columnName: selectedColumn?.columnName || currentRule.columnName || "", }; const response = await saveNumberingRuleToTest(ruleToSave); if (response.success && response.data) { const currentData = JSON.parse(JSON.stringify(response.data)) as NumberingRuleConfig; setCurrentRule(currentData); await onSave?.(response.data); toast.success("채번 규칙이 저장되었습니다"); } else { showErrorToast("채번 규칙 저장에 실패했습니다", response.error, { guidance: "설정을 확인하고 다시 시도해 주세요.", }); } } catch (error: unknown) { showErrorToast("채번 규칙 저장에 실패했습니다", error, { guidance: "설정을 확인하고 다시 시도해 주세요.", }); } finally { setLoading(false); } }, [currentRule, onSave, selectedColumn]); const selectedPart = currentRule?.parts.find((p) => p.order === selectedPartOrder) ?? null; const globalSep = currentRule?.separator ?? "-"; const partItems = currentRule ? computePartDisplayItems(currentRule) : []; return (
{/* 좌측: 채번 컬럼 목록 (테이블별 그룹화) */}
채번 컬럼 ({numberingColumns.length})
setColumnSearch(e.target.value)} placeholder="검색..." className="h-7 pl-7 text-xs" />
{loading && numberingColumns.length === 0 ? (
로딩 중...
) : filteredGroups.length === 0 ? (
{numberingColumns.length === 0 ? "채번 타입 컬럼이 없습니다" : "검색 결과 없음"}
) : ( filteredGroups.map(([tableName, group]) => (
{group.tableLabel || tableName}
{group.columns.map((col) => { const isSelected = selectedColumn?.tableName === col.tableName && selectedColumn?.columnName === col.columnName; return ( ); })}
)) )}
{/* 우측: 미리보기 + 파이프라인 + 설정 + 저장 바 */}
{!currentRule ? (

컬럼을 선택하세요

좌측에서 채번 컬럼을 선택하면 규칙을 편집할 수 있습니다

) : ( <> {/* 헤더: 규칙명 + 적용 대상 표시 */}
setCurrentRule((prev) => (prev ? { ...prev, ruleName: e.target.value } : null))} placeholder="예: 프로젝트 코드" className="h-9 text-sm" />
{selectedColumn && (
{selectedColumn.tableName}.{selectedColumn.columnName}
)}
{/* 미리보기 스트립 */}
{/* 파이프라인 영역 */}
코드 구성 {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 isPartSelected = 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 || "-"}
)}
); })} )}
{/* 설정 패널 */} {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} /> )}
)}
)} {/* 저장 바 */}
{currentRule.tableName && ( 테이블: {currentRule.tableName} )} {currentRule.columnName && ( 컬럼: {currentRule.columnName} )} 구분자: {globalSep || "-"} {currentRule.resetPeriod && currentRule.resetPeriod !== "none" && ( 리셋: {currentRule.resetPeriod} )}
)}
); };