"use client"; import React, { useState, useEffect, useMemo, useCallback, useRef } from "react"; import { Label } from "@/components/ui/label"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Checkbox } from "@/components/ui/checkbox"; import { Button } from "@/components/ui/button"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { Check, ChevronsUpDown } from "lucide-react"; import { cn } from "@/lib/utils"; import { CodePartType, DATE_FORMAT_OPTIONS } from "@/types/numbering-rule"; import { tableManagementApi } from "@/lib/api/tableManagement"; interface AutoConfigPanelProps { partType: CodePartType; config?: any; onChange: (config: any) => void; isPreview?: boolean; } interface TableInfo { tableName: string; displayName: string; } interface ColumnInfo { columnName: string; displayName: string; dataType: string; inputType?: string; } export const AutoConfigPanel: React.FC = ({ partType, config = {}, onChange, isPreview = false, }) => { // 1. 순번 (자동 증가) if (partType === "sequence") { return (
onChange({ ...config, sequenceLength: parseInt(e.target.value) || 3 }) } disabled={isPreview} className="h-8 text-xs sm:h-10 sm:text-sm" />

예: 3 → 001, 4 → 0001

onChange({ ...config, startFrom: parseInt(e.target.value) || 1 }) } disabled={isPreview} className="h-8 text-xs sm:h-10 sm:text-sm" />

순번이 시작될 번호

); } // 2. 숫자 (고정 자릿수) if (partType === "number") { return (
onChange({ ...config, numberLength: parseInt(e.target.value) || 4 }) } disabled={isPreview} className="h-8 text-xs sm:h-10 sm:text-sm" />

예: 4 → 0001, 5 → 00001

onChange({ ...config, numberValue: parseInt(e.target.value) || 0 }) } disabled={isPreview} className="h-8 text-xs sm:h-10 sm:text-sm" />

고정으로 사용할 숫자

); } // 3. 날짜 if (partType === "date") { return ( ); } // 4. 문자 if (partType === "text") { return (
onChange({ ...config, textValue: e.target.value })} placeholder="예: PRJ, CODE, PROD" disabled={isPreview} className="h-8 text-xs sm:h-10 sm:text-sm" />

고정으로 사용할 텍스트 또는 코드

); } // 5. 카테고리 if (partType === "category") { return ( ); } return null; }; /** * 날짜 타입 전용 설정 패널 * - 날짜 형식 선택 * - 컬럼 값 기준 생성 옵션 */ interface DateConfigPanelProps { config?: any; onChange: (config: any) => void; isPreview?: boolean; } const DateConfigPanel: React.FC = ({ config = {}, onChange, isPreview = false, }) => { // 테이블 목록 const [tables, setTables] = useState([]); const [loadingTables, setLoadingTables] = useState(false); const [tableComboboxOpen, setTableComboboxOpen] = useState(false); // 컬럼 목록 const [columns, setColumns] = useState([]); const [loadingColumns, setLoadingColumns] = useState(false); const [columnComboboxOpen, setColumnComboboxOpen] = useState(false); // 체크박스 상태 const useColumnValue = config.useColumnValue || false; const sourceTableName = config.sourceTableName || ""; const sourceColumnName = config.sourceColumnName || ""; // 테이블 목록 로드 useEffect(() => { if (useColumnValue && tables.length === 0) { loadTables(); } }, [useColumnValue]); // 테이블 변경 시 컬럼 로드 useEffect(() => { if (sourceTableName) { loadColumns(sourceTableName); } else { setColumns([]); } }, [sourceTableName]); const loadTables = async () => { setLoadingTables(true); try { const response = await tableManagementApi.getTableList(); if (response.success && response.data) { const tableList = response.data.map((t: any) => ({ tableName: t.tableName || t.table_name, displayName: t.displayName || t.table_label || t.tableName || t.table_name, })); setTables(tableList); } } catch (error) { console.error("테이블 목록 로드 실패:", error); } finally { setLoadingTables(false); } }; const loadColumns = async (tableName: string) => { setLoadingColumns(true); try { const response = await tableManagementApi.getColumnList(tableName); if (response.success && response.data) { const rawColumns = response.data?.columns || response.data; // 날짜 타입 컬럼만 필터링 const dateColumns = (rawColumns as any[]).filter((col: any) => { const inputType = col.inputType || col.input_type || ""; const dataType = (col.dataType || col.data_type || "").toLowerCase(); return ( inputType === "date" || inputType === "datetime" || dataType.includes("date") || dataType.includes("timestamp") ); }); setColumns( dateColumns.map((col: any) => ({ columnName: col.columnName || col.column_name, displayName: col.displayName || col.column_label || col.columnName || col.column_name, dataType: col.dataType || col.data_type || "", inputType: col.inputType || col.input_type || "", })) ); } } catch (error) { console.error("컬럼 목록 로드 실패:", error); } finally { setLoadingColumns(false); } }; // 선택된 테이블/컬럼 라벨 const selectedTableLabel = useMemo(() => { const found = tables.find((t) => t.tableName === sourceTableName); return found ? `${found.displayName} (${found.tableName})` : ""; }, [tables, sourceTableName]); const selectedColumnLabel = useMemo(() => { const found = columns.find((c) => c.columnName === sourceColumnName); return found ? `${found.displayName} (${found.columnName})` : ""; }, [columns, sourceColumnName]); return (
{/* 날짜 형식 선택 */}

{useColumnValue ? "선택한 컬럼의 날짜 값이 이 형식으로 변환됩니다" : "현재 날짜가 자동으로 입력됩니다"}

{/* 컬럼 값 기준 생성 체크박스 */}
{ onChange({ ...config, useColumnValue: checked, // 체크 해제 시 테이블/컬럼 초기화 ...(checked ? {} : { sourceTableName: "", sourceColumnName: "" }), }); }} disabled={isPreview} className="mt-0.5" />

폼에 입력된 날짜 값으로 코드를 생성합니다

{/* 테이블 선택 (체크 시 표시) */} {useColumnValue && ( <>
테이블을 찾을 수 없습니다 {tables.map((table) => ( { onChange({ ...config, sourceTableName: table.tableName, sourceColumnName: "", // 테이블 변경 시 컬럼 초기화 }); setTableComboboxOpen(false); }} className="text-xs sm:text-sm" >
{table.displayName} {table.tableName}
))}
{/* 컬럼 선택 */}
날짜 컬럼을 찾을 수 없습니다 {columns.map((column) => ( { onChange({ ...config, sourceColumnName: column.columnName }); setColumnComboboxOpen(false); }} className="text-xs sm:text-sm" >
{column.displayName} {column.columnName} ({column.inputType || column.dataType})
))}
{sourceTableName && columns.length === 0 && !loadingColumns && (

이 테이블에 날짜 타입 컬럼이 없습니다

)}
)}
); }; /** * 카테고리 타입 전용 설정 패널 * - 카테고리 선택 (테이블.컬럼) * - 카테고리 값별 형식 매핑 */ import { CategoryFormatMapping } from "@/types/numbering-rule"; import { Plus, Trash2, FolderTree } from "lucide-react"; import { getCategoryTree, getAllCategoryKeys } from "@/lib/api/categoryTree"; interface CategoryValueNode { valueId: number; valueCode: string; valueLabel: string; depth: number; path: string; parentValueId: number | null; children?: CategoryValueNode[]; } interface CategoryConfigPanelProps { config?: { categoryKey?: string; categoryMappings?: CategoryFormatMapping[]; }; onChange: (config: any) => void; isPreview?: boolean; } const CategoryConfigPanel: React.FC = ({ config = {}, onChange, isPreview = false, }) => { // 카테고리 옵션 (테이블.컬럼 + 라벨) const [categoryOptions, setCategoryOptions] = useState<{ tableName: string; columnName: string; displayName: string; displayLabel: string; // 라벨 (테이블라벨.컬럼라벨) }[]>([]); const [categoryKeyOpen, setCategoryKeyOpen] = useState(false); // 카테고리 값 트리 const [categoryValues, setCategoryValues] = useState([]); const [loadingValues, setLoadingValues] = useState(false); // 계층적 선택 상태 (대분류, 중분류, 소분류) const [level1Id, setLevel1Id] = useState(null); const [level2Id, setLevel2Id] = useState(null); const [level3Id, setLevel3Id] = useState(null); const [level1Open, setLevel1Open] = useState(false); const [level2Open, setLevel2Open] = useState(false); const [level3Open, setLevel3Open] = useState(false); // 형식 입력 const [newFormat, setNewFormat] = useState(""); // 수정 모드 const [editingId, setEditingId] = useState(null); // 수정 모드 진입 중 플래그 (useEffect 초기화 방지) const isEditingRef = useRef(false); const categoryKey = config.categoryKey || ""; const mappings = config.categoryMappings || []; // 이미 추가된 카테고리 ID 목록 (수정 중인 항목 제외) const addedValueIds = useMemo(() => { return mappings .filter(m => m.categoryValueId !== editingId) .map(m => m.categoryValueId); }, [mappings, editingId]); // 카테고리 옵션 로드 useEffect(() => { loadCategoryOptions(); }, []); // 카테고리 키 변경 시 값 로드 및 선택 초기화 useEffect(() => { if (categoryKey) { const [tableName, columnName] = categoryKey.split("."); if (tableName && columnName) { loadCategoryValues(tableName, columnName); } } else { setCategoryValues([]); } // 선택 초기화 setLevel1Id(null); setLevel2Id(null); setLevel3Id(null); }, [categoryKey]); // 대분류 변경 시 중분류/소분류 초기화 (수정 모드 진입 중에는 건너뜀) useEffect(() => { if (isEditingRef.current) return; setLevel2Id(null); setLevel3Id(null); }, [level1Id]); // 중분류 변경 시 소분류 초기화 (수정 모드 진입 중에는 건너뜀) useEffect(() => { if (isEditingRef.current) return; setLevel3Id(null); }, [level2Id]); const loadCategoryOptions = async () => { try { const response = await getAllCategoryKeys(); if (response.success && response.data) { const options = response.data.map((item: { tableName: string; columnName: string; tableLabel?: string; columnLabel?: string }) => ({ tableName: item.tableName, columnName: item.columnName, displayName: `${item.tableName}.${item.columnName}`, displayLabel: `${item.tableLabel || item.tableName}.${item.columnLabel || item.columnName}`, })); setCategoryOptions(options); } } catch (error) { console.error("카테고리 옵션 로드 실패:", error); } }; const loadCategoryValues = async (tableName: string, columnName: string) => { console.log("loadCategoryValues 호출:", { tableName, columnName }); setLoadingValues(true); try { const response = await getCategoryTree(tableName, columnName); console.log("getCategoryTree 응답:", response); if (response.success && response.data) { console.log("카테고리 트리 로드 성공:", response.data); setCategoryValues(response.data); } else { console.log("카테고리 트리 로드 실패:", response.error); setCategoryValues([]); } } catch (error) { console.error("카테고리 값 로드 실패:", error); setCategoryValues([]); } finally { setLoadingValues(false); } }; // 이미 추가된 항목 확인 (해당 노드 또는 하위 노드가 추가되었는지) const isNodeOrDescendantAdded = useCallback((node: CategoryValueNode): boolean => { if (addedValueIds.includes(node.valueId)) return true; if (node.children?.length) { return node.children.every(child => isNodeOrDescendantAdded(child)); } return false; }, [addedValueIds]); // 각 레벨별 항목 계산 (이미 추가된 항목 필터링) const level1Items = useMemo(() => { return categoryValues.filter(v => !isNodeOrDescendantAdded(v)); }, [categoryValues, isNodeOrDescendantAdded]); const level2Items = useMemo(() => { if (!level1Id) return []; const parent = categoryValues.find(v => v.valueId === level1Id); const children = parent?.children || []; return children.filter(v => !isNodeOrDescendantAdded(v)); }, [categoryValues, level1Id, isNodeOrDescendantAdded]); const level3Items = useMemo(() => { if (!level2Id) return []; const parent = categoryValues.find(v => v.valueId === level1Id); const level2Parent = parent?.children?.find(v => v.valueId === level2Id); const children = level2Parent?.children || []; return children.filter(v => !addedValueIds.includes(v.valueId)); }, [categoryValues, level1Id, level2Id, addedValueIds]); // 선택된 값 정보 계산 const getSelectedInfo = () => { // 가장 깊은 레벨의 선택된 값 const selectedId = level3Id || level2Id || level1Id; if (!selectedId) return null; // 선택된 노드 찾기 const findNode = (nodes: CategoryValueNode[], id: number): CategoryValueNode | null => { for (const node of nodes) { if (node.valueId === id) return node; if (node.children?.length) { const found = findNode(node.children, id); if (found) return found; } } return null; }; const node = findNode(categoryValues, selectedId); if (!node) return null; // 경로 생성 const pathParts: string[] = []; const l1 = categoryValues.find(v => v.valueId === level1Id); if (l1) pathParts.push(l1.valueLabel); if (level2Id) { const l2 = level2Items.find(v => v.valueId === level2Id); if (l2) pathParts.push(l2.valueLabel); } if (level3Id) { const l3 = level3Items.find(v => v.valueId === level3Id); if (l3) pathParts.push(l3.valueLabel); } return { valueId: selectedId, valueCode: node.valueCode, // valueCode 추가 (V2Select 호환) valueLabel: node.valueLabel, valuePath: pathParts.join(" > "), }; }; const selectedInfo = getSelectedInfo(); // 매핑 추가/수정 const handleAddMapping = () => { if (!selectedInfo || !newFormat.trim()) return; const newMapping: CategoryFormatMapping = { categoryValueId: selectedInfo.valueId, categoryValueCode: selectedInfo.valueCode, // V2Select에서 valueCode를 value로 사용하므로 매칭용 저장 categoryValueLabel: selectedInfo.valueLabel, categoryValuePath: selectedInfo.valuePath, format: newFormat.trim(), }; let updatedMappings: CategoryFormatMapping[]; if (editingId !== null) { // 수정 모드: 기존 항목 교체 updatedMappings = mappings.map(m => m.categoryValueId === editingId ? newMapping : m ); } else { // 추가 모드: 중복 체크 const exists = mappings.some(m => m.categoryValueId === selectedInfo.valueId); if (exists) { alert("이미 추가된 카테고리입니다"); return; } updatedMappings = [...mappings, newMapping]; } onChange({ ...config, categoryMappings: updatedMappings, }); // 초기화 setLevel1Id(null); setLevel2Id(null); setLevel3Id(null); setNewFormat(""); setEditingId(null); }; // 매핑 수정 모드 진입 const handleEditMapping = (mapping: CategoryFormatMapping) => { // useEffect 초기화 방지 플래그 설정 isEditingRef.current = true; // 해당 카테고리의 경로를 파싱해서 레벨별로 설정 const findParentIds = (nodes: CategoryValueNode[], targetId: number, path: number[] = []): number[] | null => { for (const node of nodes) { if (node.valueId === targetId) { return path; } if (node.children?.length) { const result = findParentIds(node.children, targetId, [...path, node.valueId]); if (result) return result; } } return null; }; const parentPath = findParentIds(categoryValues, mapping.categoryValueId); if (parentPath && parentPath.length > 0) { setLevel1Id(parentPath[0] || null); if (parentPath.length === 2) { // 3단계: 대분류 > 중분류 > 소분류 setLevel2Id(parentPath[1]); setLevel3Id(mapping.categoryValueId); } else if (parentPath.length === 1) { // 2단계: 대분류 > 중분류 setLevel2Id(mapping.categoryValueId); setLevel3Id(null); } else { setLevel2Id(null); setLevel3Id(null); } } else { // 루트 레벨 항목 (1단계) setLevel1Id(mapping.categoryValueId); setLevel2Id(null); setLevel3Id(null); } setNewFormat(mapping.format); setEditingId(mapping.categoryValueId); // 다음 렌더링 사이클에서 플래그 해제 setTimeout(() => { isEditingRef.current = false; }, 0); }; // 수정 취소 const handleCancelEdit = () => { setLevel1Id(null); setLevel2Id(null); setLevel3Id(null); setNewFormat(""); setEditingId(null); }; // 매핑 삭제 const handleRemoveMapping = (valueId: number) => { onChange({ ...config, categoryMappings: mappings.filter(m => m.categoryValueId !== valueId), }); }; return (
{/* 카테고리 선택 */}
카테고리가 없습니다 {categoryOptions.map((opt) => ( { onChange({ ...config, categoryKey: opt.displayName, categoryMappings: [] }); setCategoryKeyOpen(false); }} className="text-xs sm:text-sm" > {opt.displayLabel} ))}
{/* 형식 설정 */} {categoryKey && (
{/* 계층적 선택 UI */}
{/* 대분류 선택 */}
항목이 없습니다 {level1Items.map((val) => ( { setLevel1Id(val.valueId); setLevel1Open(false); }} className="text-xs" > {val.valueLabel} ))}
{/* 중분류 선택 (대분류 선택 후 하위가 있을 때만 표시) */} {level1Id && level2Items.length > 0 && (
항목이 없습니다 {level2Items.map((val) => ( { setLevel2Id(val.valueId); setLevel2Open(false); }} className="text-xs" > {val.valueLabel} ))}
)} {/* 소분류 선택 (중분류 선택 후 하위가 있을 때만 표시) */} {level2Id && level3Items.length > 0 && (
항목이 없습니다 {level3Items.map((val) => ( { setLevel3Id(val.valueId); setLevel3Open(false); }} className="text-xs" > {val.valueLabel} ))}
)}
{/* 형식 입력 + 추가/수정 버튼 */}
setNewFormat(e.target.value.toUpperCase())} placeholder="예: ITM, VLV, PIP" disabled={isPreview || !selectedInfo} className="h-8 text-xs" maxLength={10} />
{editingId !== null && ( )}
{/* 선택된 경로 표시 */} {selectedInfo && (

{editingId !== null ? "수정 중: " : "선택: "}{selectedInfo.valuePath}

)}
{/* 추가된 매핑 목록 */} {mappings.length > 0 && (
{mappings.map((m) => (
!isPreview && handleEditMapping(m)} >
{m.categoryValuePath || m.categoryValueLabel} {m.format}
))}
)}

선택된 카테고리 값에 따라 다른 형식이 생성됩니다

)}
); };