"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, FolderTree, Check, ChevronsUpDown } from "lucide-react"; import { toast } from "sonner"; import { NumberingRuleConfig, NumberingRulePart, SEPARATOR_OPTIONS, SeparatorType } from "@/types/numbering-rule"; import { NumberingRuleCard } from "./NumberingRuleCard"; import { NumberingRulePreview } from "./NumberingRulePreview"; import { saveNumberingRuleToTest, deleteNumberingRuleFromTest, getNumberingRulesFromTest, } from "@/lib/api/numberingRule"; import { getCategoryTree, getAllCategoryKeys } from "@/lib/api/categoryTree"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command"; import { cn } from "@/lib/utils"; // 카테고리 값 트리 노드 타입 interface CategoryValueNode { valueId: number; valueCode: string; valueLabel: string; depth: number; path: string; parentValueId: number | null; children?: CategoryValueNode[]; } interface NumberingRuleDesignerProps { initialConfig?: NumberingRuleConfig; onSave?: (config: NumberingRuleConfig) => void; onChange?: (config: NumberingRuleConfig) => void; maxRules?: number; isPreview?: boolean; className?: string; currentTableName?: string; // 현재 화면의 테이블명 (자동 감지용) menuObjid?: number; // 현재 메뉴 OBJID (메뉴 스코프) } export const NumberingRuleDesigner: React.FC = ({ initialConfig, onSave, onChange, maxRules = 6, isPreview = false, className = "", currentTableName, menuObjid, }) => { 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); // 구분자 관련 상태 const [separatorType, setSeparatorType] = useState("-"); const [customSeparator, setCustomSeparator] = useState(""); // 카테고리 조건 관련 상태 - 모든 카테고리를 테이블.컬럼 단위로 조회 interface CategoryOption { tableName: string; columnName: string; displayName: string; // "테이블명.컬럼명" 형식 } const [allCategoryOptions, setAllCategoryOptions] = useState([]); const [selectedCategoryKey, setSelectedCategoryKey] = useState(""); // "tableName.columnName" const [categoryValues, setCategoryValues] = useState([]); const [categoryKeyOpen, setCategoryKeyOpen] = useState(false); const [categoryValueOpen, setCategoryValueOpen] = useState(false); const [loadingCategories, setLoadingCategories] = useState(false); useEffect(() => { loadRules(); loadAllCategoryOptions(); // 전체 카테고리 옵션 로드 }, []); // currentRule의 categoryColumn이 변경되면 selectedCategoryKey 동기화 useEffect(() => { if (currentRule?.categoryColumn) { setSelectedCategoryKey(currentRule.categoryColumn); } else { setSelectedCategoryKey(""); } }, [currentRule?.categoryColumn]); // 카테고리 키 선택 시 해당 카테고리 값 로드 useEffect(() => { if (selectedCategoryKey) { const [tableName, columnName] = selectedCategoryKey.split("."); if (tableName && columnName) { loadCategoryValues(tableName, columnName); } } else { setCategoryValues([]); } }, [selectedCategoryKey]); // 전체 카테고리 옵션 로드 (모든 테이블의 category 타입 컬럼) const loadAllCategoryOptions = async () => { try { // category_values_test 테이블에서 고유한 테이블.컬럼 조합 조회 const response = await getAllCategoryKeys(); if (response.success && response.data) { const options: CategoryOption[] = response.data.map((item) => ({ tableName: item.tableName, columnName: item.columnName, displayName: `${item.tableName}.${item.columnName}`, })); setAllCategoryOptions(options); console.log("전체 카테고리 옵션 로드:", options); } } catch (error) { console.error("카테고리 옵션 목록 조회 실패:", error); } }; // 특정 카테고리 컬럼의 값 트리 조회 const loadCategoryValues = async (tableName: string, columnName: string) => { setLoadingCategories(true); try { const response = await getCategoryTree(tableName, columnName); if (response.success && response.data) { setCategoryValues(response.data); console.log("카테고리 값 로드:", { tableName, columnName, count: response.data.length }); } else { setCategoryValues([]); } } catch (error) { console.error("카테고리 값 트리 조회 실패:", error); setCategoryValues([]); } finally { setLoadingCategories(false); } }; // 카테고리 값을 플랫 리스트로 변환 (UI에서 선택용) const flattenCategoryValues = (nodes: CategoryValueNode[], result: CategoryValueNode[] = []): CategoryValueNode[] => { for (const node of nodes) { result.push(node); if (node.children && node.children.length > 0) { flattenCategoryValues(node.children, result); } } return result; }; const flatCategoryValues = flattenCategoryValues(categoryValues); const loadRules = useCallback(async () => { setLoading(true); try { console.log("🔍 [NumberingRuleDesigner] 채번 규칙 목록 로드 시작 (test 테이블):", { menuObjid, hasMenuObjid: !!menuObjid, }); // test 테이블에서 조회 const response = await getNumberingRulesFromTest(menuObjid); console.log("📦 [NumberingRuleDesigner] 채번 규칙 API 응답 (test 테이블):", { menuObjid, success: response.success, rulesCount: response.data?.length || 0, rules: response.data, }); if (response.success && response.data) { setSavedRules(response.data); } else { toast.error(response.error || "규칙 목록을 불러올 수 없습니다"); } } catch (error: any) { toast.error(`로딩 실패: ${error.message}`); } finally { setLoading(false); } }, [menuObjid]); useEffect(() => { if (currentRule) { onChange?.(currentRule); } }, [currentRule, onChange]); // currentRule이 변경될 때 구분자 상태 동기화 useEffect(() => { if (currentRule) { const sep = currentRule.separator ?? "-"; // 빈 문자열이면 "none" if (sep === "") { setSeparatorType("none"); setCustomSeparator(""); return; } // 미리 정의된 구분자인지 확인 (none, custom 제외) const predefinedOption = SEPARATOR_OPTIONS.find( opt => opt.value !== "custom" && opt.value !== "none" && opt.displayValue === sep ); if (predefinedOption) { setSeparatorType(predefinedOption.value); setCustomSeparator(""); } else { // 직접 입력된 구분자 setSeparatorType("custom"); setCustomSeparator(sep); } } }, [currentRule?.ruleId]); // ruleId가 변경될 때만 실행 (규칙 선택/생성 시) // 구분자 변경 핸들러 const handleSeparatorChange = useCallback((type: SeparatorType) => { setSeparatorType(type); if (type !== "custom") { const option = SEPARATOR_OPTIONS.find(opt => opt.value === type); const newSeparator = option?.displayValue ?? ""; setCurrentRule((prev) => prev ? { ...prev, separator: newSeparator } : null); setCustomSeparator(""); } }, []); // 직접 입력 구분자 변경 핸들러 const handleCustomSeparatorChange = useCallback((value: string) => { // 최대 2자 제한 const trimmedValue = value.slice(0, 2); setCustomSeparator(trimmedValue); setCurrentRule((prev) => prev ? { ...prev, separator: trimmedValue } : null); }, []); 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); // 파트별 기본 autoConfig 정의 const defaultAutoConfigs: Record = { sequence: { sequenceLength: 3, startFrom: 1 }, number: { numberLength: 4, numberValue: 1 }, date: { dateFormat: "YYYYMMDD" }, text: { textValue: "" }, }; // 저장 전에 각 파트의 autoConfig에 기본값 채우기 const partsWithDefaults = currentRule.parts.map((part) => { if (part.generationMethod === "auto") { const defaults = defaultAutoConfigs[part.partType] || {}; return { ...part, autoConfig: { ...defaults, ...part.autoConfig }, }; } return part; }); // 저장 전에 현재 화면의 테이블명과 menuObjid 자동 설정 // menuObjid가 있으면 menu 스코프, 없으면 기존 scopeType 유지 const effectiveMenuObjid = menuObjid || currentRule.menuObjid || null; const effectiveScopeType = effectiveMenuObjid ? "menu" : (currentRule.scopeType || "global"); const ruleToSave = { ...currentRule, parts: partsWithDefaults, scopeType: effectiveScopeType as "menu" | "global", // menuObjid 유무에 따라 결정 tableName: currentTableName || currentRule.tableName || null, // 현재 테이블명 (참고용) menuObjid: effectiveMenuObjid, // 메뉴 OBJID (필터링 기준) }; console.log("💾 채번 규칙 저장:", { currentTableName, menuObjid, "currentRule.tableName": currentRule.tableName, "currentRule.menuObjid": currentRule.menuObjid, "ruleToSave.tableName": ruleToSave.tableName, "ruleToSave.menuObjid": ruleToSave.menuObjid, "ruleToSave.scopeType": ruleToSave.scopeType, ruleToSave, }); // 테스트 테이블에 저장 (numbering_rules_test) const response = await saveNumberingRuleToTest(ruleToSave); if (response.success && response.data) { setSavedRules((prev) => { if (existing) { return prev.map((r) => (r.ruleId === ruleToSave.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, currentTableName]); 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 deleteNumberingRuleFromTest(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(() => { console.log("📋 새 규칙 생성:", { currentTableName, menuObjid }); const newRule: NumberingRuleConfig = { ruleId: `rule-${Date.now()}`, ruleName: "새 채번 규칙", parts: [], separator: "-", resetPeriod: "none", currentSequence: 1, scopeType: "table", // ⚠️ 임시: DB 제약 조건 때문에 table 유지 tableName: currentTableName || "", // 현재 화면의 테이블명 자동 설정 menuObjid: menuObjid || null, // 🆕 메뉴 OBJID 설정 (필터링용) }; console.log("📋 생성된 규칙 정보:", newRule); setSelectedRuleId(newRule.ruleId); setCurrentRule(newRule); toast.success("새 규칙이 생성되었습니다"); }, [currentTableName, menuObjid]); 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}
)) )}
{/* 구분선 */}
{/* 우측: 편집 영역 */}
{!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="예: 프로젝트 코드" />
{/* 두 번째 줄: 구분자 설정 */}
{separatorType === "custom" && (
handleCustomSeparatorChange(e.target.value)} className="h-9" placeholder="최대 2자" maxLength={2} />
)}

규칙 사이에 들어갈 문자입니다

코드 구성

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

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

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