From 128872b7664fd1024527eb99d686a57c2048f93c Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Tue, 17 Mar 2026 16:20:24 +0900 Subject: [PATCH] [agent-pipeline] pipe-20260317063830-0nfs round-1 --- .../numbering-rule/NumberingRuleDesigner.tsx | 600 +++++++++--------- .../numbering-rule/NumberingRulePreview.tsx | 208 ++++-- .../table-category/CategoryColumnList.tsx | 101 ++- .../CategoryValueManagerTree.tsx | 39 +- .../V2CategoryManagerComponent.tsx | 191 ++++-- .../SplitPanelLayoutComponent.tsx | 272 ++++++-- .../v2-table-list/SingleTableWithSticky.tsx | 107 +++- .../v2-table-list/TableListComponent.tsx | 13 +- 8 files changed, 1013 insertions(+), 518 deletions(-) diff --git a/frontend/components/numbering-rule/NumberingRuleDesigner.tsx b/frontend/components/numbering-rule/NumberingRuleDesigner.tsx index 406cd009..d85c2a83 100644 --- a/frontend/components/numbering-rule/NumberingRuleDesigner.tsx +++ b/frontend/components/numbering-rule/NumberingRuleDesigner.tsx @@ -5,28 +5,16 @@ 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, FolderTree } from "lucide-react"; +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 } from "./NumberingRulePreview"; -import { saveNumberingRuleToTest } from "@/lib/api/numberingRule"; -import { apiClient } from "@/lib/api/client"; +import { NumberingRulePreview, computePartDisplayItems, getPartTypeColorClass } from "./NumberingRulePreview"; +import { getNumberingRules, saveNumberingRuleToTest } from "@/lib/api/numberingRule"; 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; @@ -34,8 +22,8 @@ interface NumberingRuleDesignerProps { maxRules?: number; isPreview?: boolean; className?: string; - currentTableName?: string; // 현재 화면의 테이블명 (자동 감지용) - menuObjid?: number; // 현재 메뉴 OBJID (메뉴 스코프) + currentTableName?: string; + menuObjid?: number; } export const NumberingRuleDesigner: React.FC = ({ @@ -48,124 +36,84 @@ export const NumberingRuleDesigner: React.FC = ({ currentTableName, menuObjid, }) => { - const [numberingColumns, setNumberingColumns] = useState([]); - const [selectedColumn, setSelectedColumn] = useState<{ tableName: string; columnName: string } | null>(null); + 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 [columnSearch, setColumnSearch] = useState(""); - const [rightTitle, setRightTitle] = useState("규칙 편집"); - const [editingRightTitle, setEditingRightTitle] = useState(false); - - // 구분자 관련 상태 (개별 파트 사이 구분자) const [separatorTypes, setSeparatorTypes] = useState>({}); const [customSeparators, setCustomSeparators] = useState>({}); - // 좌측: 채번 타입 컬럼 목록 로드 + const selectedRule = rulesList.find((r) => r.ruleId === selectedRuleId) ?? currentRule; + + // 좌측: 규칙 목록 로드 useEffect(() => { - loadNumberingColumns(); + loadRules(); }, []); - const loadNumberingColumns = async () => { + const loadRules = async () => { setLoading(true); try { - const response = await apiClient.get("/table-management/numbering-columns"); - if (response.data.success && response.data.data) { - setNumberingColumns(response.data.data); + 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 (error: any) { - console.error("채번 컬럼 목록 로드 실패:", error); + } catch (e) { + console.error("채번 규칙 목록 로드 실패:", e); } finally { setLoading(false); } }; - // 컬럼 선택 시 해당 컬럼의 채번 규칙 로드 - const handleSelectColumn = async (tableName: string, columnName: string) => { - setSelectedColumn({ tableName, columnName }); - 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 handleSelectRule = (rule: NumberingRuleConfig) => { + setSelectedRuleId(rule.ruleId); + setCurrentRule(JSON.parse(JSON.stringify(rule))); + setSelectedPartOrder(null); }; - // 테이블별로 그룹화 - 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) - ) - ); - }); + 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); - } + if (currentRule) onChange?.(currentRule); }, [currentRule, onChange]); - // currentRule이 변경될 때 파트별 구분자 상태 동기화 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 predefinedOption = SEPARATOR_OPTIONS.find( - opt => opt.value !== "custom" && opt.value !== "none" && opt.displayValue === sep + const opt = SEPARATOR_OPTIONS.find( + (o) => o.value !== "custom" && o.value !== "none" && o.displayValue === sep ); - if (predefinedOption) { - newSepTypes[part.order] = predefinedOption.value; + if (opt) { + newSepTypes[part.order] = opt.value; newCustomSeps[part.order] = ""; } else { newSepTypes[part.order] = "custom"; @@ -173,54 +121,45 @@ export const NumberingRuleDesigner: React.FC = ({ } } }); - setSeparatorTypes(newSepTypes); setCustomSeparators(newCustomSeps); } }, [currentRule?.ruleId]); - // 개별 파트 구분자 변경 핸들러 const handlePartSeparatorChange = useCallback((partOrder: number, type: SeparatorType) => { - setSeparatorTypes(prev => ({ ...prev, [partOrder]: type })); + setSeparatorTypes((prev) => ({ ...prev, [partOrder]: type })); if (type !== "custom") { - const option = SEPARATOR_OPTIONS.find(opt => opt.value === type); + const option = SEPARATOR_OPTIONS.find((opt) => opt.value === type); const newSeparator = option?.displayValue ?? ""; - setCustomSeparators(prev => ({ ...prev, [partOrder]: "" })); + setCustomSeparators((prev) => ({ ...prev, [partOrder]: "" })); setCurrentRule((prev) => { if (!prev) return null; return { ...prev, - parts: prev.parts.map((part) => - part.order === partOrder ? { ...part, separatorAfter: newSeparator } : part - ), + 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 })); + setCustomSeparators((prev) => ({ ...prev, [partOrder]: trimmedValue })); setCurrentRule((prev) => { if (!prev) return null; return { ...prev, - parts: prev.parts.map((part) => - part.order === partOrder ? { ...part, separatorAfter: trimmedValue } : part - ), + 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, @@ -229,40 +168,33 @@ export const NumberingRuleDesigner: React.FC = ({ autoConfig: { textValue: "CODE" }, separatorAfter: "-", }; - - setCurrentRule((prev) => { - if (!prev) return null; - return { ...prev, parts: [...prev.parts, newPart] }; - }); - - // 새 파트의 구분자 상태 초기화 - setSeparatorTypes(prev => ({ ...prev, [newPart.order]: "-" })); - setCustomSeparators(prev => ({ ...prev, [newPart.order]: "" })); - + 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]); - // partOrder 기반으로 파트 업데이트 (id가 null일 수 있으므로 order 사용) const handleUpdatePart = useCallback((partOrder: number, updates: Partial) => { setCurrentRule((prev) => { if (!prev) return null; return { ...prev, - parts: prev.parts.map((part) => (part.order === partOrder ? { ...part, ...updates } : part)), + parts: prev.parts.map((p) => (p.order === partOrder ? { ...p, ...updates } : p)), }; }); }, []); - // partOrder 기반으로 파트 삭제 (id가 null일 수 있으므로 order 사용) const handleDeletePart = useCallback((partOrder: number) => { setCurrentRule((prev) => { if (!prev) return null; return { ...prev, - parts: prev.parts.filter((part) => part.order !== partOrder).map((part, index) => ({ ...part, order: index + 1 })), + parts: prev.parts + .filter((p) => p.order !== partOrder) + .map((p, i) => ({ ...p, order: i + 1 })), }; }); - + setSelectedPartOrder(null); toast.success("규칙이 삭제되었습니다"); }, []); @@ -271,246 +203,282 @@ export const NumberingRuleDesigner: React.FC = ({ toast.error("저장할 규칙이 없습니다"); return; } - if (currentRule.parts.length === 0) { toast.error("최소 1개 이상의 규칙을 추가해주세요"); return; } - setLoading(true); try { - // 파트별 기본 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, 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 || "", + scopeType: "global" as const, + tableName: currentRule.tableName || currentTableName || "", + columnName: currentRule.columnName || "", }; - - // 테스트 테이블에 저장 (numbering_rules) const response = await saveNumberingRuleToTest(ruleToSave); - if (response.success && response.data) { - const currentData = JSON.parse(JSON.stringify(response.data)) as NumberingRuleConfig; - setCurrentRule(currentData); + 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: "설정을 확인하고 다시 시도해 주세요." }); + showErrorToast("채번 규칙 저장에 실패했습니다", response.error, { + guidance: "설정을 확인하고 다시 시도해 주세요.", + }); } - } catch (error: any) { - showErrorToast("채번 규칙 저장에 실패했습니다", error, { guidance: "설정을 확인하고 다시 시도해 주세요." }); + } catch (error: unknown) { + showErrorToast("채번 규칙 저장에 실패했습니다", error, { + guidance: "설정을 확인하고 다시 시도해 주세요.", + }); } finally { setLoading(false); } - }, [currentRule, onSave, selectedColumn]); + }, [currentRule, onSave, currentTableName]); + + const selectedPart = currentRule?.parts.find((p) => p.order === selectedPartOrder) ?? null; + const globalSep = currentRule?.separator ?? "-"; + const partItems = currentRule ? computePartDisplayItems(currentRule) : []; return ( -
- {/* 좌측: 채번 컬럼 목록 (카테고리 패턴) */} -
-

채번 컬럼

- - setColumnSearch(e.target.value)} - placeholder="검색..." - className="h-8 text-xs" - /> - -
- {loading && numberingColumns.length === 0 ? ( -
-

로딩 중...

+
+ {/* 좌측: 규칙 리스트 (code-nav, 220px) */} +
+
+
+ + 채번 규칙 ({rulesList.length}) +
+ +
+
+ {loading && rulesList.length === 0 ? ( +
+ 로딩 중...
- ) : filteredGroups.length === 0 ? ( -
-

- {numberingColumns.length === 0 - ? "채번 타입 컬럼이 없습니다" - : "검색 결과가 없습니다"} -

+ ) : rulesList.length === 0 ? ( +
+ 규칙이 없습니다
) : ( - filteredGroups.map(([tableName, group]) => ( -
-
- - {group.tableLabel} - ({group.columns.length}) -
- {group.columns.map((col) => { - const isSelected = - selectedColumn?.tableName === col.tableName && - selectedColumn?.columnName === col.columnName; - return ( -
handleSelectColumn(col.tableName, col.columnName)} - > - {col.columnLabel} -
- ); - })} -
- )) + rulesList.map((rule) => { + const isSelected = selectedRuleId === rule.ruleId; + return ( + + ); + }) )}
- {/* 구분선 */} -
+
- {/* 우측: 편집 영역 */} -
+ {/* 우측: 미리보기 + 파이프라인 + 설정 + 저장 바 */} +
{!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 ? { ...prev, ruleName: e.target.value } : null))} + placeholder="예: 프로젝트 코드" + className="h-9 text-sm" + />
-
- {/* 첫 번째 줄: 규칙명 + 미리보기 */} -
-
- - setCurrentRule((prev) => ({ ...prev!, ruleName: e.target.value }))} - className="h-9" - placeholder="예: 프로젝트 코드" - /> -
-
- - -
-
- - + {/* 큰 미리보기 스트립 (code-preview-strip) */} +
+
-
-
-

코드 구성

- + {/* 파이프라인 영역 (code-pipeline-area) */} +
+
+ 코드 구성 + {currentRule.parts.length}/{maxRules}
- - {currentRule.parts.length === 0 ? ( -
-

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

-
- ) : ( -
- {currentRule.parts.map((part, index) => ( - -
- handleUpdatePart(part.order, updates)} - onDelete={() => handleDeletePart(part.order)} - isPreview={isPreview} - tableName={selectedColumn?.tableName} - /> - {/* 카드 하단에 구분자 설정 (마지막 파트 제외) */} - {index < currentRule.parts.length - 1 && ( -
- 뒤 구분자 - - {separatorTypes[part.order] === "custom" && ( - handlePartCustomSeparatorChange(part.order, e.target.value)} - className="h-6 w-14 text-center text-[10px]" - placeholder="2자" - maxLength={2} - /> +
+ {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 ( + +
- - ))} -
- )} + onClick={() => setSelectedPartOrder(part.order)} + > +
{typeLabel}
+
+ {item?.displayValue ?? "-"} +
+ + {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} + )} +
-
diff --git a/frontend/components/numbering-rule/NumberingRulePreview.tsx b/frontend/components/numbering-rule/NumberingRulePreview.tsx index eff551a1..eacdb906 100644 --- a/frontend/components/numbering-rule/NumberingRulePreview.tsx +++ b/frontend/components/numbering-rule/NumberingRulePreview.tsx @@ -1,88 +1,162 @@ "use client"; import React, { useMemo } from "react"; -import { NumberingRuleConfig } from "@/types/numbering-rule"; +import { NumberingRuleConfig, NumberingRulePart, CodePartType } from "@/types/numbering-rule"; +import { CODE_PART_TYPE_OPTIONS } from "@/types/numbering-rule"; + +/** 파트별 표시값 + 타입 (미리보기 스트립/세그먼트용) */ +export interface PartDisplayItem { + partType: CodePartType; + displayValue: string; + order: number; +} + +/** config에서 파트별 표시값 배열 계산 (정렬된 parts 기준) */ +export function computePartDisplayItems(config: NumberingRuleConfig): PartDisplayItem[] { + if (!config.parts || config.parts.length === 0) return []; + const sorted = [...config.parts].sort((a, b) => a.order - b.order); + const globalSep = config.separator ?? "-"; + return sorted.map((part) => ({ + order: part.order, + partType: part.partType, + displayValue: getPartDisplayValue(part), + })); +} + +function getPartDisplayValue(part: NumberingRulePart): string { + if (part.generationMethod === "manual") { + return part.manualConfig?.value || "XXX"; + } + const c = part.autoConfig || {}; + switch (part.partType) { + case "sequence": + return String(c.startFrom ?? 1).padStart(c.sequenceLength ?? 3, "0"); + case "number": + return String(c.numberValue ?? 0).padStart(c.numberLength ?? 4, "0"); + case "date": { + const format = c.dateFormat || "YYYYMMDD"; + if (c.useColumnValue && c.sourceColumnName) { + return format === "YYYY" ? "[YYYY]" : format === "YY" ? "[YY]" : format === "YYYYMM" ? "[YYYYMM]" : format === "YYMM" ? "[YYMM]" : format === "YYMMDD" ? "[YYMMDD]" : "[DATE]"; + } + const now = new Date(); + const y = now.getFullYear(); + const m = String(now.getMonth() + 1).padStart(2, "0"); + const d = String(now.getDate()).padStart(2, "0"); + if (format === "YYYY") return String(y); + if (format === "YY") return String(y).slice(-2); + if (format === "YYYYMM") return `${y}${m}`; + if (format === "YYMM") return `${String(y).slice(-2)}${m}`; + if (format === "YYYYMMDD") return `${y}${m}${d}`; + if (format === "YYMMDD") return `${String(y).slice(-2)}${m}${d}`; + return `${y}${m}${d}`; + } + case "text": + return c.textValue || "TEXT"; + default: + return "XXX"; + } +} + +/** 파트 타입별 미리보기용 텍스트 색상 클래스 (CSS 변수 기반) */ +export function getPartTypeColorClass(partType: CodePartType): string { + switch (partType) { + case "date": + return "text-warning"; + case "text": + return "text-primary"; + case "sequence": + return "text-primary"; + case "number": + return "text-muted-foreground"; + case "category": + case "reference": + return "text-muted-foreground"; + default: + return "text-foreground"; + } +} + +/** 파트 타입별 점(dot) 배경 색상 (범례용) */ +export function getPartTypeDotClass(partType: CodePartType): string { + switch (partType) { + case "date": + return "bg-warning"; + case "text": + case "sequence": + return "bg-primary"; + case "number": + case "category": + case "reference": + return "bg-muted-foreground"; + default: + return "bg-foreground"; + } +} interface NumberingRulePreviewProps { config: NumberingRuleConfig; compact?: boolean; + /** 큰 미리보기 스트립: 28px, 파트별 색상, 하단 범례 */ + variant?: "default" | "strip"; } export const NumberingRulePreview: React.FC = ({ config, - compact = false + compact = false, + variant = "default", }) => { + const partItems = useMemo(() => computePartDisplayItems(config), [config]); + const sortedParts = useMemo( + () => (config.parts ? [...config.parts].sort((a, b) => a.order - b.order) : []), + [config.parts] + ); const generatedCode = useMemo(() => { - if (!config.parts || config.parts.length === 0) { - return "규칙을 추가해주세요"; - } - - const sortedParts = config.parts.sort((a, b) => a.order - b.order); - - const partValues = sortedParts.map((part) => { - if (part.generationMethod === "manual") { - return part.manualConfig?.value || "XXX"; - } - - const autoConfig = part.autoConfig || {}; - - switch (part.partType) { - case "sequence": { - const length = autoConfig.sequenceLength || 3; - const startFrom = autoConfig.startFrom || 1; - return String(startFrom).padStart(length, "0"); - } - case "number": { - const length = autoConfig.numberLength || 4; - const value = autoConfig.numberValue || 0; - return String(value).padStart(length, "0"); - } - case "date": { - const format = autoConfig.dateFormat || "YYYYMMDD"; - if (autoConfig.useColumnValue && autoConfig.sourceColumnName) { - switch (format) { - case "YYYY": return "[YYYY]"; - case "YY": return "[YY]"; - case "YYYYMM": return "[YYYYMM]"; - case "YYMM": return "[YYMM]"; - case "YYYYMMDD": return "[YYYYMMDD]"; - case "YYMMDD": return "[YYMMDD]"; - default: return "[DATE]"; - } - } - const now = new Date(); - const year = now.getFullYear(); - const month = String(now.getMonth() + 1).padStart(2, "0"); - const day = String(now.getDate()).padStart(2, "0"); - switch (format) { - case "YYYY": return String(year); - case "YY": return String(year).slice(-2); - case "YYYYMM": return `${year}${month}`; - case "YYMM": return `${String(year).slice(-2)}${month}`; - case "YYYYMMDD": return `${year}${month}${day}`; - case "YYMMDD": return `${String(year).slice(-2)}${month}${day}`; - default: return `${year}${month}${day}`; - } - } - case "text": - return autoConfig.textValue || "TEXT"; - default: - return "XXX"; - } - }); - - // 파트별 개별 구분자로 결합 + if (partItems.length === 0) return "규칙을 추가해주세요"; const globalSep = config.separator ?? "-"; let result = ""; - partValues.forEach((val, idx) => { - result += val; - if (idx < partValues.length - 1) { - const sep = sortedParts[idx].separatorAfter ?? globalSep; - result += sep; + partItems.forEach((item, idx) => { + result += item.displayValue; + if (idx < partItems.length - 1) { + const part = sortedParts.find((p) => p.order === item.order); + result += part?.separatorAfter ?? globalSep; } }); return result; - }, [config]); + }, [config.separator, partItems, sortedParts]); + + if (variant === "strip") { + const globalSep = config.separator ?? "-"; + return ( +
+
+ {partItems.length === 0 ? ( + 규칙을 추가해주세요 + ) : ( + partItems.map((item, idx) => ( + + {item.displayValue} + {idx < partItems.length - 1 && ( + + {sortedParts.find((p) => p.order === item.order)?.separatorAfter ?? globalSep} + + )} + + )) + )} +
+ {partItems.length > 0 && ( +
+ {CODE_PART_TYPE_OPTIONS.filter((opt) => partItems.some((p) => p.partType === opt.value)).map((opt) => ( + + + {opt.label} + + ))} +
+ )} +
+ ); + } if (compact) { return ( diff --git a/frontend/components/table-category/CategoryColumnList.tsx b/frontend/components/table-category/CategoryColumnList.tsx index 872e7d57..2aed73fd 100644 --- a/frontend/components/table-category/CategoryColumnList.tsx +++ b/frontend/components/table-category/CategoryColumnList.tsx @@ -6,9 +6,9 @@ import { getCategoryValues } from "@/lib/api/tableCategoryValue"; import { ChevronRight, FolderTree, Loader2, Search, X } from "lucide-react"; import { Input } from "@/components/ui/input"; -interface CategoryColumn { +export interface CategoryColumn { tableName: string; - tableLabel?: string; // 테이블 라벨 추가 + tableLabel?: string; columnName: string; columnLabel: string; inputType: string; @@ -16,17 +16,30 @@ interface CategoryColumn { } interface CategoryColumnListProps { - tableName: string; // 현재 화면의 테이블 (사용하지 않음 - 형제 메뉴 전체 표시) + tableName: string; selectedColumn: string | null; - onColumnSelect: (columnName: string, columnLabel: string, tableName: string) => void; - menuObjid?: number; // 현재 메뉴 OBJID (필수) + onColumnSelect: (uniqueKeyOrColumnName: string, columnLabel: string, tableName: string) => void; + menuObjid?: number; + /** 대시보드 모드: 테이블 단위 네비만 표시, 선택 시 onTableSelect 호출 */ + selectedTable?: string | null; + onTableSelect?: (tableName: string) => void; + /** 컬럼 로드 완료 시 부모에 전달 (Stat Strip 등 계산용) */ + onColumnsLoaded?: (columns: CategoryColumn[]) => void; } /** * 카테고리 컬럼 목록 (좌측 패널) * - 형제 메뉴들의 모든 카테고리 타입 컬럼을 표시 (메뉴 스코프) */ -export function CategoryColumnList({ tableName, selectedColumn, onColumnSelect, menuObjid }: CategoryColumnListProps) { +export function CategoryColumnList({ + tableName, + selectedColumn, + onColumnSelect, + menuObjid, + selectedTable = null, + onTableSelect, + onColumnsLoaded, +}: CategoryColumnListProps) { const [columns, setColumns] = useState([]); const [isLoading, setIsLoading] = useState(false); const [searchQuery, setSearchQuery] = useState(""); @@ -151,8 +164,8 @@ export function CategoryColumnList({ tableName, selectedColumn, onColumnSelect, ); setColumns(columnsWithCount); + onColumnsLoaded?.(columnsWithCount); - // 첫 번째 컬럼 자동 선택 if (columnsWithCount.length > 0 && !selectedColumn) { const firstCol = columnsWithCount[0]; onColumnSelect(`${firstCol.tableName}.${firstCol.columnName}`, firstCol.columnLabel, firstCol.tableName); @@ -160,6 +173,7 @@ export function CategoryColumnList({ tableName, selectedColumn, onColumnSelect, } catch (error) { console.error("❌ 테이블 기반 카테고리 컬럼 조회 실패:", error); setColumns([]); + onColumnsLoaded?.([]); } finally { setIsLoading(false); } @@ -248,21 +262,20 @@ export function CategoryColumnList({ tableName, selectedColumn, onColumnSelect, } setColumns(columnsWithCount); + onColumnsLoaded?.(columnsWithCount); - // 첫 번째 컬럼 자동 선택 if (columnsWithCount.length > 0 && !selectedColumn) { const firstCol = columnsWithCount[0]; onColumnSelect(`${firstCol.tableName}.${firstCol.columnName}`, firstCol.columnLabel, firstCol.tableName); } } catch (error) { console.error("❌ 카테고리 컬럼 조회 실패:", error); - // 에러 시에도 tableName 기반으로 fallback if (tableName) { - console.log("⚠️ menuObjid API 에러, tableName 기반으로 fallback:", tableName); await loadCategoryColumnsByTable(); return; } else { setColumns([]); + onColumnsLoaded?.([]); } } setIsLoading(false); @@ -291,6 +304,72 @@ export function CategoryColumnList({ tableName, selectedColumn, onColumnSelect, ); } + // 대시보드 모드: 테이블 단위 네비만 표시 + if (onTableSelect != null) { + return ( +
+
+
+ + setSearchQuery(e.target.value)} + className="h-8 border-0 bg-transparent pl-8 pr-8 text-xs shadow-none focus-visible:ring-0" + /> + {searchQuery && ( + + )} +
+
+
+ {filteredColumns.length === 0 && searchQuery ? ( +
+ '{searchQuery}'에 대한 검색 결과가 없습니다 +
+ ) : null} + {groupedColumns.map((group) => { + const totalValues = group.columns.reduce((sum, c) => sum + (c.valueCount ?? 0), 0); + const isActive = selectedTable === group.tableName; + return ( + + ); + })} +
+
+ ); + } + return (
@@ -298,7 +377,6 @@ export function CategoryColumnList({ tableName, selectedColumn, onColumnSelect,

관리할 카테고리 컬럼을 선택하세요

- {/* 검색 입력 필드 */}
{searchQuery && ( - {/* 아이콘 */} {getIcon()} - {/* 라벨 */} -
- {node.valueLabel} - {getDepthLabel()} +
+ + {node.valueLabel} + + + {getDepthLabel()} +
- {/* 비활성 표시 */} {!node.isActive && ( - 비활성 + + 비활성 + )} - {/* 액션 버튼 */} -
+
{canAddChild && (
)} - - {/* 카테고리 값 관리 */}
{selectedColumn ? ( viewMode === "tree" ? ( @@ -130,7 +138,9 @@ export function V2CategoryManagerComponent({

- {config.showColumnList ? "좌측에서 관리할 카테고리 컬럼을 선택하세요" : "카테고리 컬럼이 설정되지 않았습니다"} + {config.showColumnList + ? "칩에서 카테고리 컬럼을 선택하세요" + : "카테고리 컬럼이 설정되지 않았습니다"}

@@ -148,24 +158,107 @@ export function V2CategoryManagerComponent({ } return ( - - } - right={rightContent} - leftTitle="카테고리 컬럼" - leftWidth={config.leftPanelWidth} - minLeftWidth={10} - maxLeftWidth={40} - height={config.height} - /> +
+ {/* Stat Strip */} +
+
+
+ {stats.columnCount} +
+
+ 카테고리 컬럼 +
+
+
+
+ {stats.totalValues} +
+
+ 전체 값 +
+
+
+
+ {stats.tableCount} +
+
+ 테이블 +
+
+
+
+ {stats.inactiveCount} +
+
+ 비활성 +
+
+
+ +
+ {/* 좌측 테이블 nav: 240px */} +
+ +
+ + {/* 우측: 칩 바 + 편집기 */} +
+ {/* 칩 바 */} +
+ {columnsForSelectedTable.map((col) => { + const uniqueKey = `${col.tableName}.${col.columnName}`; + const isActive = selectedColumn?.uniqueKey === uniqueKey; + return ( + + ); + })} + {selectedTable && columnsForSelectedTable.length === 0 && ( + 이 테이블에 카테고리 컬럼이 없습니다 + )} +
+ + {/* 편집기 영역 */} +
+ {rightContent} +
+
+
+
); } export default V2CategoryManagerComponent; - diff --git a/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutComponent.tsx b/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutComponent.tsx index c89fb1d3..6c2345a4 100644 --- a/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutComponent.tsx +++ b/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutComponent.tsx @@ -10,7 +10,6 @@ import { Badge } from "@/components/ui/badge"; import { Plus, Search, - GripVertical, Loader2, ChevronDown, ChevronUp, @@ -21,6 +20,8 @@ import { Settings, Move, FileSpreadsheet, + List, + LayoutPanelRight, } from "lucide-react"; import { dataApi } from "@/lib/api/data"; import { entityJoinApi } from "@/lib/api/entityJoin"; @@ -325,6 +326,10 @@ export const SplitPanelLayoutComponent: React.FC const [rightFilters, setRightFilters] = useState([]); const [rightGrouping, setRightGrouping] = useState([]); const [rightColumnVisibility, setRightColumnVisibility] = useState([]); + // 우측 패널 컬럼 헤더 드래그 (디자인 모드에서 순서 변경) + const [rightDraggedColumnIndex, setRightDraggedColumnIndex] = useState(null); + const [rightDropTargetColumnIndex, setRightDropTargetColumnIndex] = useState(null); + const [rightDragSource, setRightDragSource] = useState<"main" | number | null>(null); // 데이터 상태 const [leftData, setLeftData] = useState([]); @@ -2631,6 +2636,95 @@ export const SplitPanelLayoutComponent: React.FC } }, [selectedLeftItem, customLeftSelectedData, componentConfig, companyCode, toast, loadLeftData]); + // 우측 패널 컬럼 헤더 드래그 (디자인 모드에서 컬럼 순서 변경) + const handleRightColumnDragStart = useCallback( + (columnIndex: number, source: "main" | number) => { + setRightDraggedColumnIndex(columnIndex); + setRightDragSource(source); + }, + [], + ); + const handleRightColumnDragOver = useCallback((e: React.DragEvent, columnIndex: number) => { + e.preventDefault(); + e.dataTransfer.dropEffect = "move"; + setRightDropTargetColumnIndex(columnIndex); + }, []); + const handleRightColumnDragEnd = useCallback(() => { + setRightDraggedColumnIndex(null); + setRightDropTargetColumnIndex(null); + setRightDragSource(null); + }, []); + const handleRightColumnDrop = useCallback( + (e: React.DragEvent, targetIndex: number, source: "main" | number) => { + e.preventDefault(); + const fromIdx = rightDraggedColumnIndex; + if (fromIdx === null || rightDragSource !== source || fromIdx === targetIndex) { + handleRightColumnDragEnd(); + return; + } + if (!onUpdateComponent) { + handleRightColumnDragEnd(); + return; + } + const rightPanel = componentConfig.rightPanel || {}; + if (source === "main") { + const allColumns = rightPanel.columns || []; + const visibleColumns = allColumns.filter((c: any) => c.showInSummary !== false); + const hiddenColumns = allColumns.filter((c: any) => c.showInSummary === false); + if (fromIdx < 0 || fromIdx >= visibleColumns.length || targetIndex < 0 || targetIndex >= visibleColumns.length) { + handleRightColumnDragEnd(); + return; + } + const reordered = [...visibleColumns]; + const [removed] = reordered.splice(fromIdx, 1); + reordered.splice(targetIndex, 0, removed); + const columns = [...reordered, ...hiddenColumns]; + onUpdateComponent({ + ...component, + componentConfig: { + ...componentConfig, + rightPanel: { ...rightPanel, columns }, + }, + }); + } else { + const tabs = [...(rightPanel.additionalTabs || [])]; + const tabConfig = tabs[source]; + if (!tabConfig || !Array.isArray(tabConfig.columns)) { + handleRightColumnDragEnd(); + return; + } + const allTabCols = tabConfig.columns; + const visibleTabCols = allTabCols.filter((c: any) => c.showInSummary !== false); + const hiddenTabCols = allTabCols.filter((c: any) => c.showInSummary === false); + if (fromIdx < 0 || fromIdx >= visibleTabCols.length || targetIndex < 0 || targetIndex >= visibleTabCols.length) { + handleRightColumnDragEnd(); + return; + } + const reordered = [...visibleTabCols]; + const [removed] = reordered.splice(fromIdx, 1); + reordered.splice(targetIndex, 0, removed); + const columns = [...reordered, ...hiddenTabCols]; + const newTabs = tabs.map((t, i) => (i === source ? { ...t, columns } : t)); + onUpdateComponent({ + ...component, + componentConfig: { + ...componentConfig, + rightPanel: { ...rightPanel, additionalTabs: newTabs }, + }, + }); + } + handleRightColumnDragEnd(); + }, + [ + rightDraggedColumnIndex, + rightDragSource, + componentConfig, + component, + onUpdateComponent, + handleRightColumnDragEnd, + ], + ); + // 수정 모달 저장 const handleEditModalSave = useCallback(async () => { const tableName = @@ -3212,10 +3306,18 @@ export const SplitPanelLayoutComponent: React.FC alignItems: "center", }} > -
- - {componentConfig.leftPanel?.title || "좌측 패널"} - +
+
+ + + {componentConfig.leftPanel?.title || "좌측 패널"} + + {!isDesignMode && ( + + {summedLeftData.length} + + )} +
{!isDesignMode && (componentConfig.leftPanel as any)?.showBomExcelUpload && (
- {/* 리사이저 */} + {/* 리사이저: 6px 너비, 그립 핸들(2x28px bar), hover 시 primary 하이라이트 */} {resizable && (
- +
+
)} @@ -4037,9 +4141,10 @@ export const SplitPanelLayoutComponent: React.FC alignItems: "center", }} > -
-
- {/* 탭이 없으면 제목만, 있으면 탭으로 전환 */} +
+
+ + {/* 탭이 없으면 제목만, 있으면 탭으로 전환 (2px primary 밑줄 인디케이터) */} {(componentConfig.rightPanel?.additionalTabs?.length || 0) > 0 ? (
) : ( - + {componentConfig.rightPanel?.title || "우측 패널"} )} + {!isDesignMode && ( + + {activeTabIndex === 0 + ? Array.isArray(rightData) + ? rightData.length + : rightData ? 1 : 0 + : (tabsData[activeTabIndex]?.length ?? 0)} + + )}
{!isDesignMode && (
@@ -4163,16 +4277,35 @@ export const SplitPanelLayoutComponent: React.FC const hasTabActions = currentTabConfig?.showEdit || currentTabConfig?.showDelete; // showInSummary가 false가 아닌 것만 메인 테이블에 표시 const tabSummaryColumns = tabColumns.filter((col: any) => col.showInSummary !== false); + const tabIndex = activeTabIndex - 1; + const canDragTabColumns = isDesignMode && tabSummaryColumns.length > 0 && !!onUpdateComponent; return (
- {tabSummaryColumns.map((col: any) => ( - - ))} + {tabSummaryColumns.map((col: any, idx: number) => { + const isDropTarget = rightDragSource === tabIndex && rightDropTargetColumnIndex === idx; + const isDragging = rightDragSource === tabIndex && rightDraggedColumnIndex === idx; + return ( + + ); + })} {hasTabActions && ( )} @@ -4280,16 +4413,35 @@ export const SplitPanelLayoutComponent: React.FC const hasTabActions = currentTabConfig?.showEdit || currentTabConfig?.showDelete; // showInSummary가 false가 아닌 것만 메인 테이블에 표시 const listSummaryColumns = tabColumns.filter((col: any) => col.showInSummary !== false); + const listTabIndex = activeTabIndex - 1; + const canDragListTabColumns = isDesignMode && listSummaryColumns.length > 0 && !!onUpdateComponent; return (
- {col.label || col.name} - canDragTabColumns && handleRightColumnDragStart(idx, tabIndex)} + onDragOver={(e) => canDragTabColumns && handleRightColumnDragOver(e, idx)} + onDragEnd={handleRightColumnDragEnd} + onDrop={(e) => canDragTabColumns && handleRightColumnDrop(e, idx, tabIndex)} + > + {col.label || col.name} + 작업
- {listSummaryColumns.map((col: any) => ( - - ))} + {listSummaryColumns.map((col: any, idx: number) => { + const isDropTarget = rightDragSource === listTabIndex && rightDropTargetColumnIndex === idx; + const isDragging = rightDragSource === listTabIndex && rightDraggedColumnIndex === idx; + return ( + + ); + })} {hasTabActions && ( )} @@ -4672,24 +4824,43 @@ export const SplitPanelLayoutComponent: React.FC return sum + w; }, 0); + const rightConfigColumnStart = columnsToShow.filter((c: any) => c._isKeyColumn).length; + const canDragRightColumns = isDesignMode && displayColumns.length > 0 && !!onUpdateComponent; + return (
- {col.label || col.name} - canDragListTabColumns && handleRightColumnDragStart(idx, listTabIndex)} + onDragOver={(e) => canDragListTabColumns && handleRightColumnDragOver(e, idx)} + onDragEnd={handleRightColumnDragEnd} + onDrop={(e) => canDragListTabColumns && handleRightColumnDrop(e, idx, listTabIndex)} + > + {col.label || col.name} + 작업
100 ? `${rightTotalColWidth}%` : '100%' }}> - {columnsToShow.map((col, idx) => ( - - ))} + {columnsToShow.map((col, idx) => { + const configColIndex = idx - rightConfigColumnStart; + const isDraggable = canDragRightColumns && !col._isKeyColumn; + const isDropTarget = rightDragSource === "main" && rightDropTargetColumnIndex === configColIndex; + const isDragging = rightDragSource === "main" && rightDraggedColumnIndex === configColIndex; + return ( + + ); + })} {/* 수정 또는 삭제 버튼이 하나라도 활성화되어 있을 때만 작업 컬럼 표시 */} {!isDesignMode && ((componentConfig.rightPanel?.editButton?.enabled ?? true) || @@ -4705,7 +4876,7 @@ export const SplitPanelLayoutComponent: React.FC const itemId = item.id || item.ID || idx; return ( - + {columnsToShow.map((col, colIdx) => ( toggleRightItemExpansion(itemId)} @@ -4867,7 +5038,7 @@ export const SplitPanelLayoutComponent: React.FC ))} {hasActions && (
- {col.label} - isDraggable && handleRightColumnDragStart(configColIndex, "main")} + onDragOver={(e) => isDraggable && handleRightColumnDragOver(e, configColIndex)} + onDragEnd={handleRightColumnDragEnd} + onDrop={(e) => isDraggable && handleRightColumnDrop(e, configColIndex, "main")} + > + {col.label} +
{!isDesignMode && ((componentConfig.rightPanel?.editButton?.enabled ?? true) || (componentConfig.rightPanel?.deleteButton?.enabled ?? true)) && ( - -
+
+
{(componentConfig.rightPanel?.editButton?.enabled ?? true) && (
-
+
{hasEditButton && ( + )} + {hasDetailDeleteButton && ( + + )} +
+ )} {displayEntries.map(([key, value, label]) => (
diff --git a/frontend/lib/registry/components/v2-table-list/SingleTableWithSticky.tsx b/frontend/lib/registry/components/v2-table-list/SingleTableWithSticky.tsx index 69bcc9cd..c2307956 100644 --- a/frontend/lib/registry/components/v2-table-list/SingleTableWithSticky.tsx +++ b/frontend/lib/registry/components/v2-table-list/SingleTableWithSticky.tsx @@ -35,7 +35,11 @@ interface SingleTableWithStickyProps { editingValue?: string; onEditingValueChange?: (value: string) => void; onEditKeyDown?: (e: React.KeyboardEvent) => void; - editInputRef?: React.RefObject; + onEditSave?: () => void; + editInputRef?: React.RefObject; + // 인라인 편집 타입별 옵션 (select/category/code, number, date 지원) + columnMeta?: Record; + categoryMappings?: Record>; // 검색 하이라이트 관련 props searchHighlights?: Set; currentSearchIndex?: number; @@ -69,7 +73,10 @@ export const SingleTableWithSticky: React.FC = ({ editingValue, onEditingValueChange, onEditKeyDown, + onEditSave, editInputRef, + columnMeta, + categoryMappings, // 검색 하이라이트 관련 props searchHighlights, currentSearchIndex = 0, @@ -350,15 +357,19 @@ export const SingleTableWithSticky: React.FC = ({ {column.columnName === "__checkbox__" ? ( renderCheckboxCell?.(row, index) ) : isEditing ? ( - // 인라인 편집 입력 필드 - onEditingValueChange?.(e.target.value)} - onKeyDown={onEditKeyDown} - onBlur={() => { - // blur 시 저장 (Enter와 동일) + // 인라인 편집: inputType에 따라 select(category/code), number, date, text + (() => { + const meta = columnMeta?.[column.columnName]; + const inputType = meta?.inputType ?? (column as { inputType?: string }).inputType; + const isNumeric = inputType === "number" || inputType === "decimal"; + const isCategoryType = inputType === "category" || inputType === "code"; + const categoryOptions = categoryMappings?.[column.columnName]; + const hasCategoryOptions = + isCategoryType && categoryOptions && Object.keys(categoryOptions).length > 0; + + const commonInputClass = + "border-primary bg-background focus:ring-primary h-8 w-full rounded border px-2 text-xs focus:ring-2 focus:outline-none sm:text-sm"; + const handleBlurSave = () => { if (onEditKeyDown) { const fakeEvent = { key: "Enter", @@ -366,10 +377,78 @@ export const SingleTableWithSticky: React.FC = ({ } as React.KeyboardEvent; onEditKeyDown(fakeEvent); } - }} - className="border-primary bg-background focus:ring-primary h-8 w-full rounded border px-2 text-xs focus:ring-2 focus:outline-none sm:text-sm" - onClick={(e) => e.stopPropagation()} - /> + onEditSave?.(); + }; + + if (hasCategoryOptions) { + const selectOptions = Object.entries(categoryOptions).map(([value, info]) => ({ + value, + label: info.label, + })); + return ( + + ); + } + + if (inputType === "date" || inputType === "datetime") { + try { + const { InlineCellDatePicker } = require("@/components/screen/filters/InlineCellDatePicker"); + return ( + onEditingValueChange?.(v)} + onSave={() => { + handleBlurSave(); + }} + onKeyDown={onEditKeyDown} + inputRef={editInputRef as React.RefObject} + /> + ); + } catch { + return ( + } + type="text" + value={editingValue ?? ""} + onChange={(e) => onEditingValueChange?.(e.target.value)} + onKeyDown={onEditKeyDown} + onBlur={handleBlurSave} + className={commonInputClass} + onClick={(e) => e.stopPropagation()} + /> + ); + } + } + + return ( + } + type={isNumeric ? "number" : "text"} + value={editingValue ?? ""} + onChange={(e) => onEditingValueChange?.(e.target.value)} + onKeyDown={onEditKeyDown} + onBlur={handleBlurSave} + className={commonInputClass} + style={isNumeric ? { textAlign: "right" } : undefined} + onClick={(e) => e.stopPropagation()} + /> + ); + })() ) : ( renderCellContent() )} diff --git a/frontend/lib/registry/components/v2-table-list/TableListComponent.tsx b/frontend/lib/registry/components/v2-table-list/TableListComponent.tsx index 15b7a13b..f1748e9a 100644 --- a/frontend/lib/registry/components/v2-table-list/TableListComponent.tsx +++ b/frontend/lib/registry/components/v2-table-list/TableListComponent.tsx @@ -5463,6 +5463,15 @@ export const TableListComponent: React.FC = ({ }} getColumnWidth={getColumnWidth} containerWidth={calculatedWidth} + onCellDoubleClick={handleCellDoubleClick} + editingCell={editingCell} + editingValue={editingValue} + onEditingValueChange={setEditingValue} + onEditKeyDown={handleEditKeyDown} + onEditSave={saveEditing} + editInputRef={editInputRef} + columnMeta={columnMeta} + categoryMappings={categoryMappings} />
@@ -6410,7 +6419,7 @@ export const TableListComponent: React.FC = ({ onChange={(e) => setEditingValue(e.target.value)} onKeyDown={handleEditKeyDown} onBlur={saveEditing} - className="border-primary bg-background h-full w-full border-2 px-2 py-1 text-xs focus:outline-none sm:px-4 sm:py-1.5 sm:text-sm" + className="border-primary bg-background h-8 w-full border-2 px-2 py-1 text-xs focus:outline-none sm:px-4 sm:py-1.5 sm:text-sm" autoFocus > @@ -6447,7 +6456,7 @@ export const TableListComponent: React.FC = ({ onChange={(e) => setEditingValue(e.target.value)} onKeyDown={handleEditKeyDown} onBlur={saveEditing} - className="border-primary bg-background h-full w-full border-2 px-2 py-1 text-xs focus:outline-none sm:px-4 sm:py-1.5 sm:text-sm" + className="border-primary bg-background h-8 w-full border-2 px-2 py-1 text-xs focus:outline-none sm:px-4 sm:py-1.5 sm:text-sm" style={{ textAlign: isNumeric ? "right" : column.align || "left", }}