From 4db5d738178921d5c0d7f324a4c7b13b96d17aa4 Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Tue, 17 Mar 2026 14:54:45 +0900 Subject: [PATCH 01/23] [agent-pipeline] pipe-20260317054958-cypk round-1 --- .../components/screen/panels/V2PropertiesPanel.tsx | 6 +++--- .../v2/config-panels/V2FieldConfigPanel.tsx | 11 +++++++---- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/frontend/components/screen/panels/V2PropertiesPanel.tsx b/frontend/components/screen/panels/V2PropertiesPanel.tsx index ef739b27..047a10cb 100644 --- a/frontend/components/screen/panels/V2PropertiesPanel.tsx +++ b/frontend/components/screen/panels/V2PropertiesPanel.tsx @@ -211,12 +211,11 @@ export const V2PropertiesPanel: React.FC = ({ // 현재 화면의 테이블명 가져오기 const currentTableName = tables?.[0]?.tableName; - // DB input_type 가져오기 (columnMetaCache에서 최신값 조회) + // DB input_type만 조회 (saved config와 분리하여 전달) const colName = selectedComponent.columnName || currentConfig.fieldKey || currentConfig.columnName; const tblName = selectedComponent.tableName || currentTable?.tableName || currentTableName; const dbMeta = colName && tblName && !colName.includes(".") ? columnMetaCache[tblName]?.[colName] : undefined; const dbInputType = dbMeta ? (() => { const raw = dbMeta.input_type || dbMeta.inputType; return raw === "direct" || raw === "auto" ? undefined : raw; })() : undefined; - const inputType = dbInputType || currentConfig.inputType || currentConfig.webType || (selectedComponent as any).inputType; // 컴포넌트별 추가 props const extraProps: Record = {}; @@ -224,7 +223,8 @@ export const V2PropertiesPanel: React.FC = ({ const resolvedColumnName = selectedComponent.columnName || currentConfig.fieldKey || currentConfig.columnName; if (componentId === "v2-input" || componentId === "v2-select") { - extraProps.inputType = inputType; + extraProps.componentType = componentId; + extraProps.inputType = dbInputType; extraProps.tableName = resolvedTableName; extraProps.columnName = resolvedColumnName; extraProps.screenTableName = resolvedTableName; diff --git a/frontend/components/v2/config-panels/V2FieldConfigPanel.tsx b/frontend/components/v2/config-panels/V2FieldConfigPanel.tsx index 2f2b8011..3ce85266 100644 --- a/frontend/components/v2/config-panels/V2FieldConfigPanel.tsx +++ b/frontend/components/v2/config-panels/V2FieldConfigPanel.tsx @@ -77,9 +77,9 @@ interface CategoryValueOption { valueLabel: string; } -// ─── 하위 호환: 기존 config에서 fieldType 추론 ─── +// ─── 하위 호환: 기존 config에서 fieldType 추론 (우선순위: DB값 > 사용자 fieldType > 컴포넌트구조 > saved config > 기본값) ─── function resolveFieldType(config: Record, componentType?: string, metaInputType?: string): FieldType { - // DB input_type이 전달된 경우 (데이터타입관리에서 변경 시) 우선 적용 + // (a) metaInputType: DB 전용 (undefined면 스킵, V2PropertiesPanel에서 dbInputType만 전달) if (metaInputType && metaInputType !== "direct" && metaInputType !== "auto") { const dbType = metaInputType as FieldType; if (["text", "number", "textarea", "numbering", "select", "category", "entity"].includes(dbType)) { @@ -87,9 +87,10 @@ function resolveFieldType(config: Record, componentType?: string, m } } + // (b) 사용자가 설정 패널에서 직접 선택한 fieldType if (config.fieldType) return config.fieldType as FieldType; - // v2-select 계열 + // (c) v2-select 계열: componentType 또는 config.source 기반 if (componentType === "v2-select" || config.source) { const source = config.source === "code" ? "category" : config.source; if (source === "entity") return "entity"; @@ -97,11 +98,13 @@ function resolveFieldType(config: Record, componentType?: string, m return "select"; } - // v2-input 계열 + // (d) saved config fallback (config.inputType / config.type) const it = config.inputType || config.type; if (it === "number") return "number"; if (it === "textarea") return "textarea"; if (it === "numbering") return "numbering"; + + // (e) 최종 기본값 return "text"; } From 2b4500a999ea1a0595de7affc81cc066380c1889 Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Tue, 17 Mar 2026 15:00:41 +0900 Subject: [PATCH 02/23] [agent-pipeline] pipe-20260317054958-cypk round-2 --- .../screen/panels/V2PropertiesPanel.tsx | 49 +++++++++---------- 1 file changed, 24 insertions(+), 25 deletions(-) diff --git a/frontend/components/screen/panels/V2PropertiesPanel.tsx b/frontend/components/screen/panels/V2PropertiesPanel.tsx index 047a10cb..65f55230 100644 --- a/frontend/components/screen/panels/V2PropertiesPanel.tsx +++ b/frontend/components/screen/panels/V2PropertiesPanel.tsx @@ -17,7 +17,6 @@ import { GroupComponent, DataTableComponent, TableInfo, - LayoutComponent, FileComponent, AreaComponent, } from "@/types/screen"; @@ -212,15 +211,15 @@ export const V2PropertiesPanel: React.FC = ({ const currentTableName = tables?.[0]?.tableName; // DB input_type만 조회 (saved config와 분리하여 전달) - const colName = selectedComponent.columnName || currentConfig.fieldKey || currentConfig.columnName; - const tblName = selectedComponent.tableName || currentTable?.tableName || currentTableName; + const colName = (selectedComponent as any).columnName || currentConfig.fieldKey || currentConfig.columnName; + const tblName = (selectedComponent as any).tableName || currentTable?.tableName || currentTableName; const dbMeta = colName && tblName && !colName.includes(".") ? columnMetaCache[tblName]?.[colName] : undefined; const dbInputType = dbMeta ? (() => { const raw = dbMeta.input_type || dbMeta.inputType; return raw === "direct" || raw === "auto" ? undefined : raw; })() : undefined; // 컴포넌트별 추가 props const extraProps: Record = {}; - const resolvedTableName = selectedComponent.tableName || currentTable?.tableName || currentTableName; - const resolvedColumnName = selectedComponent.columnName || currentConfig.fieldKey || currentConfig.columnName; + const resolvedTableName = (selectedComponent as any).tableName || currentTable?.tableName || currentTableName; + const resolvedColumnName = (selectedComponent as any).columnName || currentConfig.fieldKey || currentConfig.columnName; if (componentId === "v2-input" || componentId === "v2-select") { extraProps.componentType = componentId; @@ -256,7 +255,7 @@ export const V2PropertiesPanel: React.FC = ({ const currentConfig = selectedComponent.componentConfig || {}; // 🔧 ConfigPanelWrapper를 인라인 함수 대신 직접 JSX 반환 (리마운트 방지) - const config = currentConfig || definition.defaultProps?.componentConfig || {}; + const config = currentConfig || (definition as any).defaultProps?.componentConfig || {}; const handlePanelConfigChange = (newConfig: any) => { // 🔧 Partial 업데이트: 기존 componentConfig를 유지하면서 새 설정만 병합 @@ -282,14 +281,14 @@ export const V2PropertiesPanel: React.FC = ({ onConfigChange={handlePanelConfigChange} tables={tables} allTables={allTables} - screenTableName={selectedComponent.tableName || currentTable?.tableName || currentTableName} - tableName={selectedComponent.tableName || currentTable?.tableName || currentTableName} + screenTableName={(selectedComponent as any).tableName || currentTable?.tableName || currentTableName} + tableName={(selectedComponent as any).tableName || currentTable?.tableName || currentTableName} columnName={ (selectedComponent as any).columnName || currentConfig?.columnName || currentConfig?.fieldName } inputType={(selectedComponent as any).inputType || currentConfig?.inputType} componentType={componentType} - tableColumns={currentTable?.columns || []} + tableColumns={(currentTable as any)?.columns || []} allComponents={allComponents} currentComponent={selectedComponent} menuObjid={menuObjid} @@ -323,8 +322,8 @@ export const V2PropertiesPanel: React.FC = ({ componentType={componentType} config={selectedComponent.componentConfig || {}} onChange={handleDynamicConfigChange} - screenTableName={selectedComponent.tableName || currentTable?.tableName || currentTableName} - tableColumns={currentTable?.columns || []} + screenTableName={(selectedComponent as any).tableName || currentTable?.tableName || currentTableName} + tableColumns={(currentTable as any)?.columns || []} tables={tables} menuObjid={menuObjid} allComponents={allComponents} @@ -491,7 +490,7 @@ export const V2PropertiesPanel: React.FC = ({ 제목
handleUpdate("title", e.target.value)} placeholder="제목" className="h-7 text-xs" @@ -503,7 +502,7 @@ export const V2PropertiesPanel: React.FC = ({ 설명
handleUpdate("description", e.target.value)} placeholder="설명" className="h-7 text-xs" @@ -519,9 +518,9 @@ export const V2PropertiesPanel: React.FC = ({

OPTIONS

{(isInputField || widget.required !== undefined) && (() => { - const colName = widget.columnName || selectedComponent?.columnName; + const colName = widget.columnName || (selectedComponent as any)?.columnName; const colMeta = colName - ? currentTable?.columns?.find( + ? (currentTable as any)?.columns?.find( (c: any) => (c.columnName || c.column_name || "").toLowerCase() === colName.toLowerCase(), ) : null; @@ -568,7 +567,7 @@ export const V2PropertiesPanel: React.FC = ({
숨김 { handleUpdate("hidden", checked); handleUpdate("componentConfig.hidden", checked); @@ -689,7 +688,7 @@ export const V2PropertiesPanel: React.FC = ({
표시 { const boolValue = checked === true; handleUpdate("style.labelDisplay", boolValue); @@ -785,7 +784,7 @@ export const V2PropertiesPanel: React.FC = ({ const webType = selectedComponent.componentConfig?.webType; // 테이블 패널에서 드래그한 컴포넌트인지 확인 - const isFromTablePanel = !!(selectedComponent.tableName && selectedComponent.columnName); + const isFromTablePanel = !!((selectedComponent as any).tableName && (selectedComponent as any).columnName); if (!componentId) { return ( @@ -845,8 +844,8 @@ export const V2PropertiesPanel: React.FC = ({ = ({ = ({ return (
{/* WebType 선택 (있는 경우만) */} - {widget.webType && ( + {(widget as any).webType && (
- handleUpdate("webType", value)}> {webTypes.map((wt) => ( - {wt.web_type_name_kor || wt.web_type} + {(wt as any).web_type_name_kor || wt.web_type} ))} From 87a7431e53209bde36d53e9435f07e298b143057 Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Tue, 17 Mar 2026 15:04:26 +0900 Subject: [PATCH 03/23] [agent-pipeline] pipe-20260317054958-cypk round-3 --- .../v2/config-panels/V2FieldConfigPanel.tsx | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/frontend/components/v2/config-panels/V2FieldConfigPanel.tsx b/frontend/components/v2/config-panels/V2FieldConfigPanel.tsx index 3ce85266..4760eae1 100644 --- a/frontend/components/v2/config-panels/V2FieldConfigPanel.tsx +++ b/frontend/components/v2/config-panels/V2FieldConfigPanel.tsx @@ -77,9 +77,17 @@ interface CategoryValueOption { valueLabel: string; } -// ─── 하위 호환: 기존 config에서 fieldType 추론 (우선순위: DB값 > 사용자 fieldType > 컴포넌트구조 > saved config > 기본값) ─── +// ─── 하위 호환: 기존 config에서 fieldType 추론 (우선순위: 컴포넌트구조 > DB값 > 사용자 fieldType > saved config > 기본값) ─── function resolveFieldType(config: Record, componentType?: string, metaInputType?: string): FieldType { - // (a) metaInputType: DB 전용 (undefined면 스킵, V2PropertiesPanel에서 dbInputType만 전달) + // (a) v2-select 계열: componentType 또는 config.source 우선 (metaInputType보다 먼저 체크) + if (componentType === "v2-select" || config.source) { + const source = config.source === "code" ? "category" : config.source; + if (source === "entity") return "entity"; + if (source === "category") return "category"; + return "select"; + } + + // (b) metaInputType: DB 전용 (undefined면 스킵, V2PropertiesPanel에서 dbInputType만 전달) if (metaInputType && metaInputType !== "direct" && metaInputType !== "auto") { const dbType = metaInputType as FieldType; if (["text", "number", "textarea", "numbering", "select", "category", "entity"].includes(dbType)) { @@ -87,17 +95,9 @@ function resolveFieldType(config: Record, componentType?: string, m } } - // (b) 사용자가 설정 패널에서 직접 선택한 fieldType + // (c) 사용자가 설정 패널에서 직접 선택한 fieldType if (config.fieldType) return config.fieldType as FieldType; - // (c) v2-select 계열: componentType 또는 config.source 기반 - if (componentType === "v2-select" || config.source) { - const source = config.source === "code" ? "category" : config.source; - if (source === "entity") return "entity"; - if (source === "category") return "category"; - return "select"; - } - // (d) saved config fallback (config.inputType / config.type) const it = config.inputType || config.type; if (it === "number") return "number"; From cc51ad71da79dc7ac5c255431410ad3e83830121 Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Tue, 17 Mar 2026 15:04:26 +0900 Subject: [PATCH 04/23] [agent-pipeline] rollback to 2b4500a9 --- .../v2/config-panels/V2FieldConfigPanel.tsx | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/frontend/components/v2/config-panels/V2FieldConfigPanel.tsx b/frontend/components/v2/config-panels/V2FieldConfigPanel.tsx index 4760eae1..3ce85266 100644 --- a/frontend/components/v2/config-panels/V2FieldConfigPanel.tsx +++ b/frontend/components/v2/config-panels/V2FieldConfigPanel.tsx @@ -77,17 +77,9 @@ interface CategoryValueOption { valueLabel: string; } -// ─── 하위 호환: 기존 config에서 fieldType 추론 (우선순위: 컴포넌트구조 > DB값 > 사용자 fieldType > saved config > 기본값) ─── +// ─── 하위 호환: 기존 config에서 fieldType 추론 (우선순위: DB값 > 사용자 fieldType > 컴포넌트구조 > saved config > 기본값) ─── function resolveFieldType(config: Record, componentType?: string, metaInputType?: string): FieldType { - // (a) v2-select 계열: componentType 또는 config.source 우선 (metaInputType보다 먼저 체크) - if (componentType === "v2-select" || config.source) { - const source = config.source === "code" ? "category" : config.source; - if (source === "entity") return "entity"; - if (source === "category") return "category"; - return "select"; - } - - // (b) metaInputType: DB 전용 (undefined면 스킵, V2PropertiesPanel에서 dbInputType만 전달) + // (a) metaInputType: DB 전용 (undefined면 스킵, V2PropertiesPanel에서 dbInputType만 전달) if (metaInputType && metaInputType !== "direct" && metaInputType !== "auto") { const dbType = metaInputType as FieldType; if (["text", "number", "textarea", "numbering", "select", "category", "entity"].includes(dbType)) { @@ -95,9 +87,17 @@ function resolveFieldType(config: Record, componentType?: string, m } } - // (c) 사용자가 설정 패널에서 직접 선택한 fieldType + // (b) 사용자가 설정 패널에서 직접 선택한 fieldType if (config.fieldType) return config.fieldType as FieldType; + // (c) v2-select 계열: componentType 또는 config.source 기반 + if (componentType === "v2-select" || config.source) { + const source = config.source === "code" ? "category" : config.source; + if (source === "entity") return "entity"; + if (source === "category") return "category"; + return "select"; + } + // (d) saved config fallback (config.inputType / config.type) const it = config.inputType || config.type; if (it === "number") return "number"; From c3fae741ae09aa199eb379fb007946a06af99b8a Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Tue, 17 Mar 2026 15:09:53 +0900 Subject: [PATCH 05/23] [agent-pipeline] pipe-20260317054958-cypk round-4 --- .../screen/panels/V2PropertiesPanel.tsx | 20 ++++++++++++++++++- .../lib/registry/DynamicComponentRenderer.tsx | 2 +- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/frontend/components/screen/panels/V2PropertiesPanel.tsx b/frontend/components/screen/panels/V2PropertiesPanel.tsx index 65f55230..a18f3bda 100644 --- a/frontend/components/screen/panels/V2PropertiesPanel.tsx +++ b/frontend/components/screen/panels/V2PropertiesPanel.tsx @@ -46,7 +46,7 @@ import { ColorPickerWithTransparent } from "../common/ColorPickerWithTransparent // ComponentRegistry import (동적 ConfigPanel 가져오기용) import { ComponentRegistry } from "@/lib/registry/ComponentRegistry"; -import { columnMetaCache } from "@/lib/registry/DynamicComponentRenderer"; +import { columnMetaCache, loadColumnMeta } from "@/lib/registry/DynamicComponentRenderer"; import { DynamicComponentConfigPanel, hasComponentConfigPanel } from "@/lib/utils/getComponentConfigPanel"; import StyleEditor from "../StyleEditor"; import { Slider } from "@/components/ui/slider"; @@ -97,6 +97,24 @@ export const V2PropertiesPanel: React.FC = ({ // 🆕 전체 테이블 목록 (selected-items-detail-input 등에서 사용) const [allTables, setAllTables] = useState>([]); + // 🆕 선택된 컴포넌트의 테이블에 대한 columnMeta 캐시가 비어 있으면 로드 후 재렌더 + const [columnMetaVersion, setColumnMetaVersion] = useState(0); + useEffect(() => { + if (!selectedComponent) return; + const tblName = + (selectedComponent as any).tableName || + currentTable?.tableName || + tables?.[0]?.tableName; + if (!tblName) return; + if (columnMetaCache[tblName]) return; + loadColumnMeta(tblName).then(() => setColumnMetaVersion((v) => v + 1)); + }, [ + selectedComponent?.id, + (selectedComponent as any)?.tableName, + currentTable?.tableName, + tables?.[0]?.tableName, + ]); + // 🆕 전체 테이블 목록 로드 useEffect(() => { const loadAllTables = async () => { diff --git a/frontend/lib/registry/DynamicComponentRenderer.tsx b/frontend/lib/registry/DynamicComponentRenderer.tsx index 46b17fe2..32cc7de1 100644 --- a/frontend/lib/registry/DynamicComponentRenderer.tsx +++ b/frontend/lib/registry/DynamicComponentRenderer.tsx @@ -30,7 +30,7 @@ export function invalidateColumnMetaCache(tableName?: string): void { } } -async function loadColumnMeta(tableName: string, forceReload = false): Promise { +export async function loadColumnMeta(tableName: string, forceReload = false): Promise { const now = Date.now(); const isStale = columnMetaTimestamp[tableName] && (now - columnMetaTimestamp[tableName] > CACHE_TTL_MS); From c55520f01c0d84cdba1e6ef01e945799ee456d97 Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Tue, 17 Mar 2026 15:09:53 +0900 Subject: [PATCH 06/23] [agent-pipeline] rollback to 87a7431e --- .../screen/panels/V2PropertiesPanel.tsx | 20 +------------------ .../lib/registry/DynamicComponentRenderer.tsx | 2 +- 2 files changed, 2 insertions(+), 20 deletions(-) diff --git a/frontend/components/screen/panels/V2PropertiesPanel.tsx b/frontend/components/screen/panels/V2PropertiesPanel.tsx index a18f3bda..65f55230 100644 --- a/frontend/components/screen/panels/V2PropertiesPanel.tsx +++ b/frontend/components/screen/panels/V2PropertiesPanel.tsx @@ -46,7 +46,7 @@ import { ColorPickerWithTransparent } from "../common/ColorPickerWithTransparent // ComponentRegistry import (동적 ConfigPanel 가져오기용) import { ComponentRegistry } from "@/lib/registry/ComponentRegistry"; -import { columnMetaCache, loadColumnMeta } from "@/lib/registry/DynamicComponentRenderer"; +import { columnMetaCache } from "@/lib/registry/DynamicComponentRenderer"; import { DynamicComponentConfigPanel, hasComponentConfigPanel } from "@/lib/utils/getComponentConfigPanel"; import StyleEditor from "../StyleEditor"; import { Slider } from "@/components/ui/slider"; @@ -97,24 +97,6 @@ export const V2PropertiesPanel: React.FC = ({ // 🆕 전체 테이블 목록 (selected-items-detail-input 등에서 사용) const [allTables, setAllTables] = useState>([]); - // 🆕 선택된 컴포넌트의 테이블에 대한 columnMeta 캐시가 비어 있으면 로드 후 재렌더 - const [columnMetaVersion, setColumnMetaVersion] = useState(0); - useEffect(() => { - if (!selectedComponent) return; - const tblName = - (selectedComponent as any).tableName || - currentTable?.tableName || - tables?.[0]?.tableName; - if (!tblName) return; - if (columnMetaCache[tblName]) return; - loadColumnMeta(tblName).then(() => setColumnMetaVersion((v) => v + 1)); - }, [ - selectedComponent?.id, - (selectedComponent as any)?.tableName, - currentTable?.tableName, - tables?.[0]?.tableName, - ]); - // 🆕 전체 테이블 목록 로드 useEffect(() => { const loadAllTables = async () => { diff --git a/frontend/lib/registry/DynamicComponentRenderer.tsx b/frontend/lib/registry/DynamicComponentRenderer.tsx index 32cc7de1..46b17fe2 100644 --- a/frontend/lib/registry/DynamicComponentRenderer.tsx +++ b/frontend/lib/registry/DynamicComponentRenderer.tsx @@ -30,7 +30,7 @@ export function invalidateColumnMetaCache(tableName?: string): void { } } -export async function loadColumnMeta(tableName: string, forceReload = false): Promise { +async function loadColumnMeta(tableName: string, forceReload = false): Promise { const now = Date.now(); const isStale = columnMetaTimestamp[tableName] && (now - columnMetaTimestamp[tableName] > CACHE_TTL_MS); From d8a542b25375a7f3cb17bf0b36a4a20329ae0ef7 Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Tue, 17 Mar 2026 15:13:42 +0900 Subject: [PATCH 07/23] [agent-pipeline] pipe-20260317054958-cypk round-5 --- .../screen/panels/V2PropertiesPanel.tsx | 20 ++++++++++++++++++- .../lib/registry/DynamicComponentRenderer.tsx | 6 ++++-- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/frontend/components/screen/panels/V2PropertiesPanel.tsx b/frontend/components/screen/panels/V2PropertiesPanel.tsx index 65f55230..a18f3bda 100644 --- a/frontend/components/screen/panels/V2PropertiesPanel.tsx +++ b/frontend/components/screen/panels/V2PropertiesPanel.tsx @@ -46,7 +46,7 @@ import { ColorPickerWithTransparent } from "../common/ColorPickerWithTransparent // ComponentRegistry import (동적 ConfigPanel 가져오기용) import { ComponentRegistry } from "@/lib/registry/ComponentRegistry"; -import { columnMetaCache } from "@/lib/registry/DynamicComponentRenderer"; +import { columnMetaCache, loadColumnMeta } from "@/lib/registry/DynamicComponentRenderer"; import { DynamicComponentConfigPanel, hasComponentConfigPanel } from "@/lib/utils/getComponentConfigPanel"; import StyleEditor from "../StyleEditor"; import { Slider } from "@/components/ui/slider"; @@ -97,6 +97,24 @@ export const V2PropertiesPanel: React.FC = ({ // 🆕 전체 테이블 목록 (selected-items-detail-input 등에서 사용) const [allTables, setAllTables] = useState>([]); + // 🆕 선택된 컴포넌트의 테이블에 대한 columnMeta 캐시가 비어 있으면 로드 후 재렌더 + const [columnMetaVersion, setColumnMetaVersion] = useState(0); + useEffect(() => { + if (!selectedComponent) return; + const tblName = + (selectedComponent as any).tableName || + currentTable?.tableName || + tables?.[0]?.tableName; + if (!tblName) return; + if (columnMetaCache[tblName]) return; + loadColumnMeta(tblName).then(() => setColumnMetaVersion((v) => v + 1)); + }, [ + selectedComponent?.id, + (selectedComponent as any)?.tableName, + currentTable?.tableName, + tables?.[0]?.tableName, + ]); + // 🆕 전체 테이블 목록 로드 useEffect(() => { const loadAllTables = async () => { diff --git a/frontend/lib/registry/DynamicComponentRenderer.tsx b/frontend/lib/registry/DynamicComponentRenderer.tsx index 46b17fe2..70c74a1e 100644 --- a/frontend/lib/registry/DynamicComponentRenderer.tsx +++ b/frontend/lib/registry/DynamicComponentRenderer.tsx @@ -30,9 +30,11 @@ export function invalidateColumnMetaCache(tableName?: string): void { } } -async function loadColumnMeta(tableName: string, forceReload = false): Promise { +export async function loadColumnMeta(tableName: string, forceReload = false): Promise { const now = Date.now(); - const isStale = columnMetaTimestamp[tableName] && (now - columnMetaTimestamp[tableName] > CACHE_TTL_MS); + const cachedAt = columnMetaTimestamp[tableName]; + const isStale = + typeof cachedAt === "number" && now - cachedAt > CACHE_TTL_MS; if (!forceReload && !isStale && columnMetaCache[tableName]) return; From 80cd95e68398fec3ce28c5091d7ea9f73ef35a8d Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Tue, 17 Mar 2026 15:18:41 +0900 Subject: [PATCH 08/23] [agent-pipeline] pipe-20260317054958-cypk round-6 --- .../lib/registry/DynamicComponentRenderer.tsx | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/frontend/lib/registry/DynamicComponentRenderer.tsx b/frontend/lib/registry/DynamicComponentRenderer.tsx index 70c74a1e..8a1873f7 100644 --- a/frontend/lib/registry/DynamicComponentRenderer.tsx +++ b/frontend/lib/registry/DynamicComponentRenderer.tsx @@ -36,14 +36,14 @@ export async function loadColumnMeta(tableName: string, forceReload = false): Pr const isStale = typeof cachedAt === "number" && now - cachedAt > CACHE_TTL_MS; - if (!forceReload && !isStale && columnMetaCache[tableName]) return; + if (!forceReload && !isStale && tableName in columnMetaCache && columnMetaCache[tableName]) return; if (forceReload || isStale) { delete columnMetaCache[tableName]; delete columnMetaLoading[tableName]; } - if (columnMetaLoading[tableName]) { + if (tableName in columnMetaLoading) { await columnMetaLoading[tableName]; return; } @@ -665,7 +665,8 @@ export const DynamicComponentRenderer: React.FC = } // 1. 새 컴포넌트 시스템에서 먼저 조회 - const newComponent = ComponentRegistry.getComponent(componentType); + const newComponent = + componentType != null ? ComponentRegistry.getComponent(componentType) : null; if (newComponent) { // 새 컴포넌트 시스템으로 렌더링 @@ -777,7 +778,7 @@ export const DynamicComponentRenderer: React.FC = // 렌더러 props 구성 // 숨김 값 추출 - const hiddenValue = component.hidden || component.componentConfig?.hidden; + const hiddenValue = (component as any).hidden || component.componentConfig?.hidden; // 숨김 처리: 인터랙티브 모드(실제 뷰)에서만 숨김, 디자인 모드에서는 표시 if (hiddenValue && isInteractive) { @@ -898,7 +899,7 @@ export const DynamicComponentRenderer: React.FC = // 새로운 기능들 전달 // 🆕 webTypeConfig.numberingRuleId가 있으면 autoGeneration으로 변환 autoGeneration: - component.autoGeneration || + (component as any).autoGeneration || component.componentConfig?.autoGeneration || ((component as any).webTypeConfig?.numberingRuleId ? { @@ -998,7 +999,8 @@ export const DynamicComponentRenderer: React.FC = let renderedElement: React.ReactElement; if (isClass) { - const rendererInstance = new NewComponentRenderer(rendererProps); + const RendererClass = NewComponentRenderer as new (props: any) => { render: () => React.ReactElement }; + const rendererInstance = new RendererClass(rendererProps); renderedElement = rendererInstance.render(); } else { renderedElement = ; @@ -1010,7 +1012,9 @@ export const DynamicComponentRenderer: React.FC = const labelFontSize = component.style?.labelFontSize || "14px"; const labelColor = getAdaptiveLabelColor(component.style?.labelColor); const labelFontWeight = component.style?.labelFontWeight || "500"; - const isRequired = effectiveComponent.required || isColumnRequiredByMeta(screenTableName, baseColumnName); + const isRequired = + effectiveComponent.required || + isColumnRequiredByMeta(screenTableName ?? "", baseColumnName ?? ""); const isLeft = labelPosition === "left"; return ( @@ -1044,7 +1048,8 @@ export const DynamicComponentRenderer: React.FC = } // 2. 레거시 시스템에서 조회 - const renderer = legacyComponentRegistry.get(componentType); + const renderer = + componentType != null ? legacyComponentRegistry.get(componentType) : undefined; if (!renderer) { console.error(`⚠️ 등록되지 않은 컴포넌트 타입: ${componentType}`, { From 128872b7664fd1024527eb99d686a57c2048f93c Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Tue, 17 Mar 2026 16:20:24 +0900 Subject: [PATCH 09/23] [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 && ( ))} {hasGroupedLeftActions && ( - )} @@ -3621,7 +3621,7 @@ export const SplitPanelLayoutComponent: React.FC {columnsToShow.map((col, idx) => ( ))} {hasLeftTableActions && ( - )} @@ -3972,17 +3972,17 @@ export const SplitPanelLayoutComponent: React.FC >
- + {/* 탭이 없으면 제목만, 있으면 탭으로 전환 (2px primary 밑줄 인디케이터) */} {(componentConfig.rightPanel?.additionalTabs?.length || 0) > 0 ? (
+ )} @@ -4157,13 +4157,13 @@ export const SplitPanelLayoutComponent: React.FC toggleRightItemExpansion(`tab_${activeTabIndex}_${tabItemId}`)} > {tabSummaryColumns.map((col: any) => ( - + )} @@ -4292,13 +4292,13 @@ export const SplitPanelLayoutComponent: React.FC toggleRightItemExpansion(`tab_${activeTabIndex}_${tabItemId}`)} > {listSummaryColumns.map((col: any) => ( - ); })} - {/* 수정 또는 삭제 버튼이 하나라도 활성화되어 있을 때만 작업 컬럼 표시 */} - {!isDesignMode && - ((componentConfig.rightPanel?.editButton?.enabled ?? true) || - (componentConfig.rightPanel?.deleteButton?.enabled ?? true)) && ( - - )} + ) : null; + })()} {filteredData.map((item, idx) => { const itemId = item.id || item.ID || idx; + const rightEditVisible = (componentConfig.rightPanel?.showEdit ?? componentConfig.rightPanel?.editButton?.enabled) !== false; + const rightDeleteVisible = (componentConfig.rightPanel?.showDelete ?? componentConfig.rightPanel?.deleteButton?.enabled) !== false; return ( - + {columnsToShow.map((col, colIdx) => ( ))} {/* 수정 또는 삭제 버튼이 하나라도 활성화되어 있을 때만 작업 셀 표시 */} - {!isDesignMode && - ((componentConfig.rightPanel?.editButton?.enabled ?? true) || - (componentConfig.rightPanel?.deleteButton?.enabled ?? true)) && ( - ))} {hasActions && ( - + )} @@ -4849,13 +4850,13 @@ export const SplitPanelLayoutComponent: React.FC toggleRightItemExpansion(itemId)} > {columnsToDisplay.map((col) => ( - ))} {hasActions && ( - )} {visibleColumns.map((column, columnIndex) => { @@ -5856,11 +5891,12 @@ export const TableListComponent: React.FC = ({ key={column.columnName} ref={(el) => (columnRefs.current[column.columnName] = el)} className={cn( - "text-foreground/90 relative h-8 overflow-hidden text-xs font-bold text-ellipsis whitespace-nowrap select-none sm:h-10 sm:text-sm", - column.columnName === "__checkbox__" ? "px-0 py-1" : "px-2 py-1 sm:px-4 sm:py-2", + "group text-muted-foreground relative h-8 overflow-hidden text-[10px] font-bold uppercase tracking-[0.04em] text-ellipsis whitespace-nowrap select-none sm:h-10 sm:text-xs", + column.columnName === "__checkbox__" ? "px-0 py-1" : "px-3 py-2", column.sortable !== false && column.columnName !== "__checkbox__" && - "hover:bg-muted/70 cursor-pointer transition-colors", + "hover:text-foreground hover:bg-muted/70 cursor-pointer transition-colors", + sortColumn === column.columnName && "!text-primary", isFrozen && "sticky z-40 shadow-[2px_0_4px_rgba(0,0,0,0.1)]", // 🆕 Column Reordering 스타일 isColumnDragEnabled && @@ -5880,7 +5916,7 @@ export const TableListComponent: React.FC = ({ minWidth: column.columnName === "__checkbox__" ? "48px" : undefined, maxWidth: column.columnName === "__checkbox__" ? "48px" : undefined, userSelect: "none", - backgroundColor: "hsl(var(--muted))", + backgroundColor: "hsl(var(--muted) / 0.8)", ...(isFrozen && { left: `${leftPosition}px` }), }} // 🆕 Column Reordering 이벤트 @@ -5900,9 +5936,12 @@ export const TableListComponent: React.FC = ({ renderCheckboxHeader() ) : (
+ {isColumnDragEnabled && ( + + )} {columnLabels[column.columnName] || column.displayName} {column.sortable !== false && sortColumn === column.columnName && ( - {sortDirection === "asc" ? "↑" : "↓"} + {sortDirection === "asc" ? "↑" : "↓"} )} {/* 🆕 헤더 필터 버튼 */} {tableConfig.headerFilter !== false && @@ -6127,7 +6166,8 @@ export const TableListComponent: React.FC = ({
handleRowClick(row, index, e)} > @@ -6158,13 +6198,14 @@ export const TableListComponent: React.FC = ({ = ({ data-row={index} data-col={colIndex} className={cn( - "text-foreground text-xs font-normal sm:text-sm", - // 이미지 컬럼은 overflow/ellipsis 제외 (이미지 잘림 방지) - inputType !== "image" && "overflow-hidden text-ellipsis whitespace-nowrap", - column.columnName === "__checkbox__" ? "px-0 py-1" : "px-2 py-1 sm:px-4 sm:py-1.5", + "text-foreground text-[11px] font-normal", + inputType !== "image" && "overflow-hidden text-ellipsis whitespace-nowrap max-w-[170px]", + column.columnName === "__checkbox__" ? "px-0 py-[7px]" : "px-3 py-[7px]", isFrozen && "sticky z-20 shadow-[2px_0_4px_rgba(0,0,0,0.08)]", - // 🆕 포커스된 셀 스타일 isCellFocused && !editingCell && "ring-primary bg-primary/5 ring-2 ring-inset", - // 🆕 편집 중인 셀 스타일 editingCell?.rowIndex === index && editingCell?.colIndex === colIndex && "p-0", - // 🆕 배치 편집: 수정된 셀 스타일 (노란 배경) isModified && !cellValidationError && "bg-amber-100 dark:bg-amber-900/40", - // 🆕 유효성 에러: 빨간 테두리 및 배경 cellValidationError && "bg-red-50 ring-2 ring-red-500 ring-inset dark:bg-red-950/40", - // 🆕 검색 하이라이트 스타일 (노란 배경) isSearchHighlighted && !isCellFocused && "bg-yellow-200 dark:bg-yellow-700/50", - // 🆕 편집 불가 컬럼 스타일 (연한 회색 배경) column.editable === false && "bg-gray-50 dark:bg-gray-900/30", + // 코드 컬럼: mono 폰트 + primary 색상 + (inputType === "code" || inputType === "category") && "font-mono text-[10px] text-primary font-medium", + // 숫자 컬럼: tabular-nums 오른쪽 정렬 + isNumeric && "tabular-nums", )} // 🆕 유효성 에러 툴팁 title={cellValidationError || undefined} @@ -6465,7 +6504,9 @@ export const TableListComponent: React.FC = ({ })() : column.columnName === "__checkbox__" ? renderCheckboxCell(row, index) - : formatCellValue(cellValue, column, row)} + : (cellValue === null || cellValue === undefined || cellValue === "") + ? - + : formatCellValue(cellValue, column, row)} ); })} diff --git a/frontend/lib/registry/components/v2-tabs-widget/tabs-component.tsx b/frontend/lib/registry/components/v2-tabs-widget/tabs-component.tsx index 03de3cc1..039a591c 100644 --- a/frontend/lib/registry/components/v2-tabs-widget/tabs-component.tsx +++ b/frontend/lib/registry/components/v2-tabs-widget/tabs-component.tsx @@ -48,8 +48,8 @@ const TabsDesignEditor: React.FC<{ return cn( "px-4 py-2 text-sm font-medium cursor-pointer transition-colors", isActive - ? "bg-primary/10 border-b-2 border-primary text-primary font-semibold" - : "text-foreground/70 hover:text-foreground hover:bg-muted/50" + ? "bg-primary/20 dark:bg-primary/25 border-b-2 border-primary text-primary font-semibold" + : "text-muted-foreground hover:text-foreground hover:bg-muted/50" ); }; From 13b2ebaf1f4c26dfcd0f25718d17df254475ee35 Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Tue, 17 Mar 2026 22:02:52 +0900 Subject: [PATCH 17/23] Refactor ColumnDetailPanel and AppLayout for improved loading state handling and UI consistency. Enhance TabBar and TableListComponent styles for better user experience. Update V2SplitPanelLayoutConfigPanel to manage button visibility based on configuration. Introduce filter chips in TableListComponent for better filter management. --- .../screen/RealtimePreviewDynamic.tsx | 21 +-- .../ButtonPrimaryComponent.tsx | 2 +- .../SplitPanelLayoutComponent.tsx | 166 +++++++++++------- .../v2-table-list/SingleTableWithSticky.tsx | 8 +- .../v2-table-list/TableListComponent.tsx | 19 +- 5 files changed, 124 insertions(+), 92 deletions(-) diff --git a/frontend/components/screen/RealtimePreviewDynamic.tsx b/frontend/components/screen/RealtimePreviewDynamic.tsx index d23337b5..14314a61 100644 --- a/frontend/components/screen/RealtimePreviewDynamic.tsx +++ b/frontend/components/screen/RealtimePreviewDynamic.tsx @@ -359,20 +359,10 @@ const RealtimePreviewDynamicComponent: React.FC = ({ return `${actualHeight}px`; } - // 런타임 모드에서 컴포넌트 타입별 높이 처리 + // 런타임 모드: ResponsiveGridRenderer가 ratio 기반으로 래퍼 높이를 설정하므로, + // 안쪽 컴포넌트는 "100%"로 래퍼를 채워야 비율이 정확하게 맞음 if (!isDesignMode) { const compType = (component as any).componentType || component.componentConfig?.type || ""; - // 테이블: 부모 flex 컨테이너가 높이 관리 (flex: 1) - const flexGrowTypes = [ - "table-list", "v2-table-list", - "split-panel-layout", "split-panel-layout2", - "v2-split-panel-layout", "screen-split-panel", - "v2-tab-container", "tab-container", - "tabs-widget", "v2-tabs-widget", - ]; - if (flexGrowTypes.some(t => compType === t)) { - return "100%"; - } const autoHeightTypes = [ "table-search-widget", "v2-table-search-widget", "flow-widget", @@ -380,9 +370,11 @@ const RealtimePreviewDynamicComponent: React.FC = ({ if (autoHeightTypes.some(t => compType === t || compType.includes(t))) { return "auto"; } + // 나머지 모든 타입: 래퍼의 비율 스케일링을 따르도록 100% + return "100%"; } - // 1순위: size.height가 있으면 우선 사용 + // 디자인 모드: 고정 픽셀 사용 (캔버스 내 절대 좌표 배치) if (size?.height && size.height > 0) { if (component.componentConfig?.type === "table-list") { return `${Math.max(size.height, 200)}px`; @@ -390,17 +382,14 @@ const RealtimePreviewDynamicComponent: React.FC = ({ return `${size.height}px`; } - // 2순위: componentStyle.height (컴포넌트 정의에서 온 기본 스타일) if (componentStyle?.height) { return typeof componentStyle.height === "number" ? `${componentStyle.height}px` : componentStyle.height; } - // 3순위: 기본값 if (component.componentConfig?.type === "table-list") { return "200px"; } - // 기본 높이 return "10px"; }; diff --git a/frontend/lib/registry/components/v2-button-primary/ButtonPrimaryComponent.tsx b/frontend/lib/registry/components/v2-button-primary/ButtonPrimaryComponent.tsx index 26a5d7c4..dc3dccc0 100644 --- a/frontend/lib/registry/components/v2-button-primary/ButtonPrimaryComponent.tsx +++ b/frontend/lib/registry/components/v2-button-primary/ButtonPrimaryComponent.tsx @@ -1410,7 +1410,7 @@ export const ButtonPrimaryComponent: React.FC = ({ const buttonElementStyle: React.CSSProperties = { width: buttonWidth, height: buttonHeight, - minHeight: "32px", // 🔧 최소 높이를 32px로 줄임 + minHeight: undefined, // 비율 스케일링 시 래퍼 높이를 정확히 따르도록 제거 // 커스텀 테두리 스타일 (StyleEditor 설정 우선, shorthand 사용 안 함) borderWidth: style?.borderWidth || "0", borderStyle: (style?.borderStyle as React.CSSProperties["borderStyle"]) || (style?.borderWidth ? "solid" : "none"), 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 0c585587..40f00e1a 100644 --- a/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutComponent.tsx +++ b/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutComponent.tsx @@ -22,6 +22,7 @@ import { FileSpreadsheet, List, PanelRight, + GripVertical, } from "lucide-react"; import { dataApi } from "@/lib/api/data"; import { entityJoinApi } from "@/lib/api/entityJoin"; @@ -313,10 +314,11 @@ 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 [runtimeColumnOrder, setRuntimeColumnOrder] = useState>({}); // 데이터 상태 const [leftData, setLeftData] = useState([]); @@ -2544,55 +2546,67 @@ export const SplitPanelLayoutComponent: React.FC 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; + + if (onUpdateComponent) { + // 디자인 모드: config에 영구 저장 + 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 }, + }, + }); } - 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 }, - }, + // 런타임 모드: 로컬 상태로 순서 변경 + const key = String(source); + setRuntimeColumnOrder((prev) => { + const existing = prev[key]; + const maxLen = 100; + const order = existing || Array.from({ length: maxLen }, (_, i) => i); + const reordered = [...order]; + const [removed] = reordered.splice(fromIdx, 1); + reordered.splice(targetIndex, 0, removed); + return { ...prev, [key]: reordered }; }); } handleRightColumnDragEnd(); @@ -2604,9 +2618,29 @@ export const SplitPanelLayoutComponent: React.FC component, onUpdateComponent, handleRightColumnDragEnd, + setRuntimeColumnOrder, ], ); + // 런타임 컬럼 순서 적용 헬퍼 + const applyRuntimeOrder = useCallback( + (columns: T[], source: "main" | number): T[] => { + const key = String(source); + const order = runtimeColumnOrder[key]; + if (!order) return columns; + const result: T[] = []; + for (const idx of order) { + if (idx < columns.length) result.push(columns[idx]); + } + // order에 없는 나머지 컬럼 추가 + for (let i = 0; i < columns.length; i++) { + if (!order.includes(i)) result.push(columns[i]); + } + return result.length > 0 ? result : columns; + }, + [runtimeColumnOrder], + ); + // 수정 모달 저장 const handleEditModalSave = useCallback(async () => { const tableName = @@ -3946,11 +3980,10 @@ export const SplitPanelLayoutComponent: React.FC {resizable && (
-
-
+
)} @@ -4107,7 +4140,7 @@ export const SplitPanelLayoutComponent: React.FC // showInSummary가 false가 아닌 것만 메인 테이블에 표시 const tabSummaryColumns = tabColumns.filter((col: any) => col.showInSummary !== false); const tabIndex = activeTabIndex - 1; - const canDragTabColumns = isDesignMode && tabSummaryColumns.length > 0 && !!onUpdateComponent; + const canDragTabColumns = tabSummaryColumns.length > 0; return (
- {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", }} From 265f46f8d4afe11aa1a8dd823ce813678351c991 Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Tue, 17 Mar 2026 16:47:12 +0900 Subject: [PATCH 10/23] [agent-pipeline] pipe-20260317063830-0nfs round-2 --- .../numbering-rule/AutoConfigPanel.tsx | 2 +- .../numbering-rule/NumberingRuleCard.tsx | 40 +++++----- .../numbering-rule/NumberingRuleDesigner.tsx | 79 ++++++++++--------- .../numbering-rule/NumberingRulePreview.tsx | 9 ++- .../table-category/CategoryValueAddDialog.tsx | 2 +- .../CategoryValueEditDialog.tsx | 2 +- .../table-category/CategoryValueManager.tsx | 1 - .../CategoryValueManagerTree.tsx | 10 ++- .../V2CategoryManagerComponent.tsx | 8 +- .../SplitPanelLayoutComponent.tsx | 10 +-- .../v2-table-list/SingleTableWithSticky.tsx | 3 +- .../v2-table-list/TableListComponent.tsx | 6 +- 12 files changed, 88 insertions(+), 84 deletions(-) diff --git a/frontend/components/numbering-rule/AutoConfigPanel.tsx b/frontend/components/numbering-rule/AutoConfigPanel.tsx index ac4d2ffa..0586006a 100644 --- a/frontend/components/numbering-rule/AutoConfigPanel.tsx +++ b/frontend/components/numbering-rule/AutoConfigPanel.tsx @@ -478,7 +478,7 @@ const DateConfigPanel: React.FC = ({ {sourceTableName && columns.length === 0 && !loadingColumns && ( -

+

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

)} diff --git a/frontend/components/numbering-rule/NumberingRuleCard.tsx b/frontend/components/numbering-rule/NumberingRuleCard.tsx index e3dbc3ab..568ecbdc 100644 --- a/frontend/components/numbering-rule/NumberingRuleCard.tsx +++ b/frontend/components/numbering-rule/NumberingRuleCard.tsx @@ -1,7 +1,6 @@ "use client"; import React from "react"; -import { Card, CardContent, CardHeader } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; @@ -27,25 +26,24 @@ export const NumberingRuleCard: React.FC = ({ tableName, }) => { return ( - - -
- - 규칙 {part.order} - - -
-
+
+
+ + 규칙 {part.order} + + +
- +
= ({
{/* 큰 미리보기 스트립 (code-preview-strip) */} -
+
{/* 파이프라인 영역 (code-pipeline-area) */} -
-
- 코드 구성 - +
+
+ 코드 구성 + {currentRule.parts.length}/{maxRules}
-
+
{currentRule.parts.length === 0 ? (
규칙을 추가하여 코드를 구성하세요 @@ -376,7 +375,7 @@ export const NumberingRuleDesigner: React.FC = ({ {index < currentRule.parts.length - 1 && ( - - - - {sep || "(없음)"} +
+ + + {sep || "-"} - +
)} ); })} + {headerRight != null ?
{headerRight}
: null}
diff --git a/frontend/components/table-category/CategoryValueManagerTree.tsx b/frontend/components/table-category/CategoryValueManagerTree.tsx index a31c054a..3d361ff0 100644 --- a/frontend/components/table-category/CategoryValueManagerTree.tsx +++ b/frontend/components/table-category/CategoryValueManagerTree.tsx @@ -60,6 +60,8 @@ interface CategoryValueManagerTreeProps { columnName: string; columnLabel: string; onValueCountChange?: (count: number) => void; + /** 편집기 헤더 오른쪽에 표시할 내용 (예: 트리/목록 세그먼트) */ + headerRight?: React.ReactNode; } // 트리 노드 컴포넌트 @@ -272,6 +274,7 @@ export const CategoryValueManagerTree: React.FC = columnName, columnLabel, onValueCountChange, + headerRight, }) => { // 상태 const [tree, setTree] = useState([]); @@ -634,7 +637,7 @@ export const CategoryValueManagerTree: React.FC = return (
- {/* 편집기 헤더: 컬럼명 + 값 수 Badge + 선택 Badge + 액션 버튼 */} + {/* 편집기 헤더: 컬럼명 + 값 수 Badge + 비활성/전체펼침/대분류추가 + headerRight(트리·목록 세그먼트 등) */}

{columnLabel} 카테고리

@@ -668,6 +671,7 @@ export const CategoryValueManagerTree: React.FC = 대분류 추가 + {headerRight != null ?
{headerRight}
: null}
diff --git a/frontend/lib/registry/components/v2-category-manager/V2CategoryManagerComponent.tsx b/frontend/lib/registry/components/v2-category-manager/V2CategoryManagerComponent.tsx index 3e951102..22485ec9 100644 --- a/frontend/lib/registry/components/v2-category-manager/V2CategoryManagerComponent.tsx +++ b/frontend/lib/registry/components/v2-category-manager/V2CategoryManagerComponent.tsx @@ -88,33 +88,33 @@ export function V2CategoryManagerComponent({ [columns, selectedTable], ); + /** 편집기 헤더에 표시할 트리/목록 세그먼트 (보기 방식 토글) */ + const viewModeSegment = + config.showViewModeToggle ? ( +
+ + +
+ ) : null; + const rightContent = ( <> - {config.showViewModeToggle && ( -
- 보기 방식: -
- - -
-
- )}
{selectedColumn ? ( viewMode === "tree" ? ( @@ -123,6 +123,7 @@ export function V2CategoryManagerComponent({ tableName={selectedColumn.tableName} columnName={selectedColumn.columnName} columnLabel={selectedColumn.columnLabel} + headerRight={viewModeSegment} /> ) : ( ) ) : ( 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 19f6f611..ae6c4fe4 100644 --- a/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutComponent.tsx +++ b/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutComponent.tsx @@ -207,19 +207,6 @@ export const SplitPanelLayoutComponent: React.FC }) => { const componentConfig = (component.componentConfig || {}) as SplitPanelLayoutConfig; - // 🐛 디버깅: 로드 시 rightPanel.components 확인 - const rightComps = componentConfig.rightPanel?.components || []; - const finishedTimeline = rightComps.find((c: any) => c.id === "finished_timeline"); - if (finishedTimeline) { - const fm = finishedTimeline.componentConfig?.fieldMapping; - console.log("🔍 [SplitPanelLayout] finished_timeline fieldMapping:", { - componentId: finishedTimeline.id, - fieldMapping: fm ? JSON.stringify(fm) : "undefined", - fieldMappingKeys: fm ? Object.keys(fm) : [], - fieldMappingId: fm?.id, - fullComponentConfig: JSON.stringify(finishedTimeline.componentConfig || {}, null, 2), - }); - } // 🆕 프리뷰용 회사 코드 오버라이드 (최고 관리자만 사용 가능) const companyCode = (props as any).companyCode as string | undefined; @@ -635,14 +622,6 @@ export const SplitPanelLayoutComponent: React.FC } = splitPanelContext; const splitPanelId = `split-panel-${component.id}`; - // 디버깅: Context 연결 상태 확인 - console.log("🔗 [SplitPanelLayout] Context 연결 상태:", { - componentId: component.id, - splitPanelId, - hasRegisterFunc: typeof ctxRegisterSplitPanel === "function", - splitPanelsSize: splitPanelContext.splitPanels?.size ?? "없음", - }); - // Context에 분할 패널 등록 (좌표 정보 포함) - 마운트 시 1회만 실행 const ctxRegisterRef = useRef(ctxRegisterSplitPanel); const ctxUnregisterRef = useRef(ctxUnregisterSplitPanel); @@ -666,15 +645,9 @@ export const SplitPanelLayoutComponent: React.FC isDragging: false, }; - console.log("📦 [SplitPanelLayout] Context에 분할 패널 등록:", { - splitPanelId, - panelInfo, - }); - ctxRegisterRef.current(splitPanelId, panelInfo); return () => { - console.log("📦 [SplitPanelLayout] Context에서 분할 패널 해제:", splitPanelId); ctxUnregisterRef.current(splitPanelId); }; // 마운트/언마운트 시에만 실행, 위치/크기 변경은 별도 업데이트로 처리 @@ -731,10 +704,6 @@ export const SplitPanelLayoutComponent: React.FC isDragging: false, initialLeftWidthPercent: leftWidth, }); - console.log("🛑 [SplitPanelLayout] 드래그 종료 - 버튼 위치 고정:", { - splitPanelId, - finalLeftWidthPercent: leftWidth, - }); } prevIsDraggingRef.current = isDragging; @@ -742,11 +711,8 @@ export const SplitPanelLayoutComponent: React.FC // 🆕 그룹별 합산된 데이터 계산 const summedLeftData = useMemo(() => { - console.log("🔍 [그룹합산] leftGroupSumConfig:", leftGroupSumConfig); - // 그룹핑이 비활성화되었거나 그룹 기준 컬럼이 없으면 원본 데이터 반환 if (!leftGroupSumConfig?.enabled || !leftGroupSumConfig?.groupByColumn) { - console.log("🔍 [그룹합산] 그룹핑 비활성화 - 원본 데이터 반환"); return leftData; } @@ -759,7 +725,6 @@ export const SplitPanelLayoutComponent: React.FC const [refTable, fieldName] = columnName.split("."); const inferredSourceColumn = refTable.replace("_info", "_code").replace("_mng", "_id"); const exactKey = `${inferredSourceColumn}_${fieldName}`; - console.log("🔍 [그룹합산] 조인 컬럼 키 변환:", { columnName, exactKey, hasKey: item[exactKey] !== undefined }); if (item[exactKey] !== undefined) return exactKey; if (fieldName === "item_name" || fieldName === "name") { const aliasKey = `${inferredSourceColumn}_name`; @@ -812,14 +777,7 @@ export const SplitPanelLayoutComponent: React.FC } }); - const result = Array.from(groupMap.values()); - console.log("🔗 [분할패널] 그룹별 합산 결과:", { - 원본개수: leftData.length, - 그룹개수: result.length, - 그룹기준: groupByColumn, - }); - - return result; + return Array.from(groupMap.values()); }, [leftData, leftGroupSumConfig]); // 컴포넌트 스타일 @@ -1262,8 +1220,6 @@ export const SplitPanelLayoutComponent: React.FC leftTableName, ); - console.log("🔗 [분할패널] 좌측 additionalJoinColumns:", leftJoinColumns); - const result = await entityJoinApi.getTableDataWithJoins(leftTableName, { page: 1, size: 100, @@ -1274,12 +1230,6 @@ export const SplitPanelLayoutComponent: React.FC companyCodeOverride: companyCode, }); - // 🔍 디버깅: API 응답 데이터의 키 확인 - if (result.data && result.data.length > 0) { - console.log("🔗 [분할패널] API 응답 첫 번째 데이터 키:", Object.keys(result.data[0])); - console.log("🔗 [분할패널] API 응답 첫 번째 데이터:", result.data[0]); - } - // 좌측 패널 dataFilter 클라이언트 사이드 적용 let filteredLeftData = result.data || []; const leftDataFilter = componentConfig.leftPanel?.dataFilter; @@ -1453,11 +1403,8 @@ export const SplitPanelLayoutComponent: React.FC // 🆕 그룹 합산된 항목인 경우: 원본 데이터들로 우측 패널 표시 if (leftItem._originalItems && leftItem._originalItems.length > 0) { - console.log("🔗 [분할패널] 그룹 합산 항목 - 원본 개수:", leftItem._originalItems.length); - // 정렬 기준 컬럼 (복합키의 leftColumn들) const sortColumns = keys?.map((k: any) => k.leftColumn).filter(Boolean) || []; - console.log("🔗 [분할패널] 정렬 기준 컬럼:", sortColumns); // 정렬 함수 const sortByKeys = (data: any[]) => { @@ -1476,7 +1423,6 @@ export const SplitPanelLayoutComponent: React.FC // 원본 데이터를 그대로 우측 패널에 표시 (이력 테이블과 동일 테이블인 경우) if (leftTable === rightTableName) { const sortedData = sortByKeys(leftItem._originalItems); - console.log("🔗 [분할패널] 동일 테이블 - 정렬된 원본 데이터:", sortedData.length); setRightData(sortedData); return; } @@ -1514,9 +1460,7 @@ export const SplitPanelLayoutComponent: React.FC } } - // 정렬 적용 const sortedResults = sortByKeys(allResults); - console.log("🔗 [분할패널] 그룹 합산 - 우측 패널 정렬된 데이터:", sortedResults.length); setRightData(sortedResults); return; } @@ -1534,17 +1478,11 @@ export const SplitPanelLayoutComponent: React.FC } }); - console.log("🔗 [분할패널] 복합키 조건:", searchConditions); - // 🆕 우측 패널 config의 Entity 조인 컬럼 추출 const rightJoinColumns = extractAdditionalJoinColumns( componentConfig.rightPanel?.columns, rightTableName, ); - if (rightJoinColumns) { - console.log("🔗 [분할패널] 우측 패널 additionalJoinColumns:", rightJoinColumns); - } - // 엔티티 조인 API로 데이터 조회 const result = await entityJoinApi.getTableDataWithJoins(rightTableName, { search: searchConditions, @@ -1554,8 +1492,6 @@ export const SplitPanelLayoutComponent: React.FC additionalJoinColumns: rightJoinColumns, }); - console.log("🔗 [분할패널] 복합키 조회 결과:", result); - setRightData(result.data || []); } else { // 단일키 (하위 호환성) → entityJoinApi 사용으로 전환 (entity 조인 컬럼 지원) @@ -1566,8 +1502,6 @@ export const SplitPanelLayoutComponent: React.FC const leftValue = leftItem[leftColumn]; const { entityJoinApi } = await import("@/lib/api/entityJoin"); - console.log("🔗 [분할패널] 단일키 조건:", { leftColumn, rightColumn, leftValue, rightTableName }); - // 단일키를 복합키 형식으로 변환 (entity 컬럼이므로 equals 연산자 필수) const searchConditions: Record = {}; searchConditions[rightColumn] = { value: leftValue, operator: "equals" }; @@ -1577,10 +1511,6 @@ export const SplitPanelLayoutComponent: React.FC componentConfig.rightPanel?.columns, rightTableName, ); - if (rightJoinColumnsLegacy) { - console.log("🔗 [분할패널] 단일키 모드 additionalJoinColumns:", rightJoinColumnsLegacy); - } - const result = await entityJoinApi.getTableDataWithJoins(rightTableName, { search: searchConditions, enableEntityJoin: true, @@ -1631,10 +1561,6 @@ export const SplitPanelLayoutComponent: React.FC // 탭 config의 Entity 조인 컬럼 추출 const tabJoinColumns = extractAdditionalJoinColumns(tabConfig.columns, tabTableName); - if (tabJoinColumns) { - console.log(`🔗 [분할패널] 탭 ${tabIndex} additionalJoinColumns:`, tabJoinColumns); - } - let resultData: any[] = []; // 탭의 dataFilter (API 전달용) @@ -1830,7 +1756,6 @@ export const SplitPanelLayoutComponent: React.FC if (leftTableName && !isDesignMode) { import("@/stores/modalDataStore").then(({ useModalDataStore }) => { useModalDataStore.getState().setData(leftTableName, [item]); - console.log(`✅ 분할 패널 좌측 선택: ${leftTableName}`, item); }); } }, @@ -2051,7 +1976,6 @@ export const SplitPanelLayoutComponent: React.FC } }); setLeftColumnLabels(labels); - console.log("✅ 좌측 컬럼 라벨 로드:", labels); } catch (error) { console.error("좌측 테이블 컬럼 라벨 로드 실패:", error); } @@ -2214,8 +2138,6 @@ export const SplitPanelLayoutComponent: React.FC }); }); - console.log("🔍 우측 패널 카테고리 로드 대상 테이블:", Array.from(tablesToLoad)); - // 각 테이블에 대해 카테고리 매핑 로드 for (const tableName of tablesToLoad) { try { @@ -2245,9 +2167,6 @@ export const SplitPanelLayoutComponent: React.FC // 🆕 컬럼명만으로도 접근할 수 있도록 추가 저장 (모든 테이블) // 기존 매핑이 있으면 병합, 없으면 새로 생성 mappings[columnName] = { ...(mappings[columnName] || {}), ...valueMap }; - - console.log(`✅ 우측 카테고리 매핑 로드 [${mappingKey}]:`, valueMap); - console.log(`✅ 우측 카테고리 매핑 (컬럼명만) [${columnName}]:`, mappings[columnName]); } } catch (error) { console.error(`우측 카테고리 값 조회 실패 [${tableName}.${columnName}]:`, error); @@ -2302,10 +2221,6 @@ export const SplitPanelLayoutComponent: React.FC }), ); - console.log("✅ [SplitPanel] 좌측 추가 모달 화면 열기:", { - screenId: addButtonConfig.modalScreenId, - tableName: leftTableName, - }); return; } } @@ -2372,11 +2287,6 @@ export const SplitPanelLayoutComponent: React.FC }), ); - console.log("✅ [SplitPanel] 추가 모달 화면 열기:", { - screenId: addButtonConfig.modalScreenId, - tableName: currentTableName, - parentData, - }); return; } } @@ -2445,11 +2355,6 @@ export const SplitPanelLayoutComponent: React.FC }), ); - console.log("✅ [SplitPanel] 좌측 수정 모달 화면 열기:", { - screenId: editButtonConfig.modalScreenId, - tableName: leftTableName, - primaryKeyValue, - }); return; } } @@ -2501,14 +2406,6 @@ export const SplitPanelLayoutComponent: React.FC } } - console.log("✅ 수정 모달 열기:", { - tableName: rightTableName, - primaryKeyName, - primaryKeyValue, - screenId: modalScreenId, - fullItem: item, - }); - // modalDataStore에도 저장 (호환성 유지) import("@/stores/modalDataStore").then(({ useModalDataStore }) => { useModalDataStore.getState().setData(rightTableName, [item]); @@ -2517,12 +2414,6 @@ export const SplitPanelLayoutComponent: React.FC // 🆕 groupByColumns 추출 const groupByColumns = componentConfig.rightPanel?.editButton?.groupByColumns || []; - console.log("🔧 [SplitPanel] 수정 버튼 클릭 - groupByColumns 확인:", { - groupByColumns, - editButtonConfig: componentConfig.rightPanel?.editButton, - hasGroupByColumns: groupByColumns.length > 0, - }); - // ScreenModal 열기 이벤트 발생 (URL 파라미터로 ID + groupByColumns 전달) window.dispatchEvent( new CustomEvent("openScreenModal", { @@ -2540,13 +2431,6 @@ export const SplitPanelLayoutComponent: React.FC }), ); - console.log("✅ [SplitPanel] openScreenModal 이벤트 발생:", { - screenId: modalScreenId, - editId: primaryKeyValue, - tableName: rightTableName, - groupByColumns: groupByColumns.length > 0 ? JSON.stringify(groupByColumns) : "없음", - }); - return; } } @@ -2606,8 +2490,6 @@ export const SplitPanelLayoutComponent: React.FC cleanData.company_code = companyCode; } - console.log("📝 [SplitPanel] 커스텀 우측 패널 저장:", { tableName, primaryKey, data: cleanData }); - const response = await dataApi.updateRecord(tableName, primaryKey, cleanData); if (response.success) { @@ -2743,8 +2625,6 @@ export const SplitPanelLayoutComponent: React.FC } try { - console.log("📝 데이터 수정:", { tableName, primaryKey, data: editModalFormData }); - // 프론트엔드 전용 필드 제거 (children, level 등) const cleanData = { ...editModalFormData }; delete cleanData.children; @@ -2761,7 +2641,6 @@ export const SplitPanelLayoutComponent: React.FC rightColumn: componentConfig.rightPanel.relation.rightColumn, oldLeftValue: editModalItem[componentConfig.rightPanel.relation.leftColumn], }; - console.log("🔗 조인 관계 정보 추가:", updatePayload._relationInfo); } const result = await dataApi.updateRecord(tableName, primaryKey, updatePayload); @@ -2831,7 +2710,6 @@ export const SplitPanelLayoutComponent: React.FC // 우측 패널 + 중계 테이블 모드인 경우 if (deleteModalPanel === "right" && componentConfig.rightPanel?.addConfig?.targetTable) { tableName = componentConfig.rightPanel.addConfig.targetTable; - console.log("🔗 중계 테이블 모드: 삭제 대상 테이블 =", tableName); } } @@ -2841,9 +2719,6 @@ export const SplitPanelLayoutComponent: React.FC if (!primaryKey && deleteModalItem && typeof deleteModalItem === "object") { // id가 없는 경우에만 전체 객체 전달 (복합키 테이블) primaryKey = deleteModalItem; - console.log("🔑 복합키: 전체 객체 전달", Object.keys(primaryKey)); - } else { - console.log("🔑 단일키 삭제: id =", primaryKey, "테이블 =", tableName); } if (!tableName || !primaryKey) { @@ -2856,16 +2731,6 @@ export const SplitPanelLayoutComponent: React.FC } try { - console.log("🗑️ 데이터 삭제:", { tableName, primaryKey }); - - // 🔍 중복 제거 설정 디버깅 - console.log("🔍 중복 제거 디버깅:", { - panel: deleteModalPanel, - dataFilter: componentConfig.rightPanel?.dataFilter, - deduplication: componentConfig.rightPanel?.dataFilter?.deduplication, - enabled: componentConfig.rightPanel?.dataFilter?.deduplication?.enabled, - }); - let result; // 🔧 중복 제거가 활성화된 경우, groupByColumn 기준으로 모든 관련 레코드 삭제 @@ -2875,7 +2740,6 @@ export const SplitPanelLayoutComponent: React.FC if (groupByColumn && deleteModalItem[groupByColumn]) { const groupValue = deleteModalItem[groupByColumn]; - console.log(`🔗 중복 제거 활성화: ${groupByColumn} = ${groupValue} 기준으로 모든 레코드 삭제`); // groupByColumn 값으로 필터링하여 삭제 const filterConditions: Record = { @@ -2889,8 +2753,6 @@ export const SplitPanelLayoutComponent: React.FC filterConditions[rightColumn] = selectedLeftItem[leftColumn]; } - console.log("🗑️ 그룹 삭제 조건:", filterConditions); - // 그룹 삭제 API 호출 result = await dataApi.deleteGroupRecords(tableName, filterConditions); } else { @@ -3022,7 +2884,6 @@ export const SplitPanelLayoutComponent: React.FC if (addConfig.leftPanelColumn && addConfig.targetColumn && selectedLeftItem) { const leftValue = selectedLeftItem[addConfig.leftPanelColumn]; finalData[addConfig.targetColumn] = leftValue; - console.log(`🔗 좌측 패널 값 자동 채움: ${addConfig.targetColumn} = ${leftValue}`); } // 자동 채움 컬럼 추가 @@ -3030,7 +2891,6 @@ export const SplitPanelLayoutComponent: React.FC Object.entries(addConfig.autoFillColumns).forEach(([key, value]) => { finalData[key] = value; }); - console.log("🔧 자동 채움 컬럼:", addConfig.autoFillColumns); } } else { // 일반 테이블 모드 @@ -3066,8 +2926,6 @@ export const SplitPanelLayoutComponent: React.FC } try { - console.log("📝 데이터 추가:", { tableName, data: finalData }); - const result = await dataApi.createRecord(tableName, finalData); if (result.success) { @@ -3210,7 +3068,6 @@ export const SplitPanelLayoutComponent: React.FC useEffect(() => { const handleRefreshTable = () => { if (!isDesignMode) { - console.log("🔄 [SplitPanel] refreshTable 이벤트 수신 - 데이터 새로고침"); loadLeftData(); // 현재 활성 탭 데이터 새로고침 (좌측 미선택 시에도 전체 데이터 로드) if (activeTabIndex === 0) { @@ -3510,7 +3367,6 @@ export const SplitPanelLayoutComponent: React.FC }} // 🆕 중첩된 탭 내부 컴포넌트 선택 핸들러 - 부모 분할 패널 정보 포함 onSelectTabComponent={(tabId: string, compId: string, tabComp: any) => { - console.log("🔍 [SplitPanel-Left] onSelectTabComponent 호출:", { tabId, compId, tabComp, parentSplitPanelId: component.id }); // 탭 내 컴포넌트 선택 상태 업데이트 setNestedTabSelectedCompId(compId); // 부모 분할 패널 정보와 함께 전역 이벤트 발생 @@ -3606,12 +3462,6 @@ export const SplitPanelLayoutComponent: React.FC (() => { // 🆕 그룹별 합산된 데이터 사용 const dataSource = summedLeftData; - console.log( - "🔍 [테이블모드 렌더링] dataSource 개수:", - dataSource.length, - "leftGroupSumConfig:", - leftGroupSumConfig, - ); // 🔧 로컬 검색 필터 적용 const filteredData = leftSearchQuery @@ -3898,12 +3748,6 @@ export const SplitPanelLayoutComponent: React.FC (() => { // 🆕 그룹별 합산된 데이터 사용 const dataToDisplay = summedLeftData; - console.log( - "🔍 [렌더링] dataToDisplay 개수:", - dataToDisplay.length, - "leftGroupSumConfig:", - leftGroupSumConfig, - ); // 검색 필터링 (클라이언트 사이드) const filteredLeftData = leftSearchQuery @@ -3930,13 +3774,6 @@ export const SplitPanelLayoutComponent: React.FC const configuredColumns = componentConfig.leftPanel?.columns || []; let displayFields: { label: string; value: any }[] = []; - // 디버그 로그 - if (index === 0) { - console.log("🔍 좌측 패널 표시 로직:"); - console.log(" - 설정된 표시 컬럼:", configuredColumns); - console.log(" - item keys:", Object.keys(item)); - } - if (configuredColumns.length > 0) { // 🔧 "표시할 컬럼 선택"에서 설정한 컬럼 사용 displayFields = configuredColumns.slice(0, 2).map((col: any) => { @@ -3960,10 +3797,6 @@ export const SplitPanelLayoutComponent: React.FC value: displayValue, }; }); - - if (index === 0) { - console.log(" ✅ 설정된 컬럼 기반 표시:", displayFields); - } } else { // 설정된 컬럼이 없으면 자동으로 첫 2개 필드 표시 const keys = Object.keys(item).filter( @@ -3984,10 +3817,6 @@ export const SplitPanelLayoutComponent: React.FC value: displayValue, }; }); - - if (index === 0) { - console.log(" ⚠️ 설정된 컬럼 없음, 자동 선택:", displayFields); - } } const displayTitle = displayFields[0]?.value || item.name || item.title || `항목 ${index + 1}`; @@ -4801,7 +4630,6 @@ export const SplitPanelLayoutComponent: React.FC _isKeyColumn: true, // 구분용 플래그 })); columnsToShow = [...keyColsToAdd, ...columnsToShow]; - console.log("🔗 [우측패널] 그룹모드 - 키 컬럼 추가:", missingKeyColumns); } } } else { @@ -5120,39 +4948,24 @@ export const SplitPanelLayoutComponent: React.FC let displayEntries: [string, any, string][] = []; if (rightColumns && rightColumns.length > 0) { - console.log("🔍 [디버깅] 상세 모드 표시 로직:"); - console.log(" 📋 rightData 전체:", rightData); - console.log(" 📋 rightData keys:", Object.keys(rightData)); - console.log( - " ⚙️ 설정된 컬럼:", - rightColumns.map((c) => `${c.name} (${c.label})`), - ); - // 설정된 컬럼만 표시 (showInDetail이 false가 아닌 것만) displayEntries = rightColumns .filter((col) => col.showInDetail !== false) .map((col) => { - // 🆕 엔티티 조인 컬럼 처리 (예: item_info.item_name → item_name) let value = rightData[col.name]; - console.log(` 🔎 컬럼 "${col.name}": 직접 접근 = ${value}`); if (value === undefined && col.name.includes(".")) { const columnName = col.name.split(".").pop(); value = rightData[columnName || ""]; - console.log(` → 변환 후 "${columnName}" 접근 = ${value}`); } return [col.name, value, col.label] as [string, any, string]; - }) -; // 설정된 컬럼은 null/empty여도 항상 표시 - - console.log(" ✅ 최종 표시할 항목:", displayEntries.length, "개"); + }); } else { // 설정 없으면 모든 컬럼 표시 displayEntries = Object.entries(rightData) .filter(([_, value]) => value !== null && value !== undefined && value !== "") .map(([key, value]) => [key, value, ""] as [string, any, string]); - console.log(" ⚠️ 컬럼 설정 없음, 모든 컬럼 표시"); } const hasDetailEditButton = !isDesignMode && (componentConfig.rightPanel?.editButton?.enabled ?? true); diff --git a/frontend/lib/registry/components/v2-table-list/SingleTableWithSticky.tsx b/frontend/lib/registry/components/v2-table-list/SingleTableWithSticky.tsx index 8bfb07f6..7f73e487 100644 --- a/frontend/lib/registry/components/v2-table-list/SingleTableWithSticky.tsx +++ b/frontend/lib/registry/components/v2-table-list/SingleTableWithSticky.tsx @@ -381,6 +381,7 @@ export const SingleTableWithSticky: React.FC = ({ onEditSave?.(); }; + // category/code 타입: select는 반드시 h-8(32px)로 행 높이 유지 if (hasCategoryOptions) { const selectOptions = Object.entries(categoryOptions).map(([value, info]) => ({ value, @@ -393,7 +394,7 @@ export const SingleTableWithSticky: React.FC = ({ onChange={(e) => onEditingValueChange?.(e.target.value)} onKeyDown={onEditKeyDown} onBlur={handleBlurSave} - className={commonInputClass} + className={cn(commonInputClass, "h-8")} onClick={(e) => e.stopPropagation()} > From d3acf391a40962aeb92c21cbf778d6ef1773ffdb Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Tue, 17 Mar 2026 18:05:10 +0900 Subject: [PATCH 12/23] [agent-pipeline] pipe-20260317084014-ydap round-1 --- .../admin/systemMng/tableMngList/page.tsx | 27 ++++++--- .../admin/table-type/ColumnDetailPanel.tsx | 48 +++++++++++++--- .../admin/table-type/ColumnGrid.tsx | 48 +++++++++++++--- frontend/components/layout/AppLayout.tsx | 56 +++++++++++++++++-- .../table-category/CategoryColumnList.tsx | 1 + frontend/components/ui/tabs.tsx | 4 +- .../SplitPanelLayoutComponent.tsx | 8 +-- .../v2-tabs-widget/tabs-component.tsx | 12 ++-- 8 files changed, 164 insertions(+), 40 deletions(-) diff --git a/frontend/app/(main)/admin/systemMng/tableMngList/page.tsx b/frontend/app/(main)/admin/systemMng/tableMngList/page.tsx index 44051e28..d98c2115 100644 --- a/frontend/app/(main)/admin/systemMng/tableMngList/page.tsx +++ b/frontend/app/(main)/admin/systemMng/tableMngList/page.tsx @@ -217,10 +217,16 @@ export default function TableManagementPage() { // 메모이제이션된 입력타입 옵션 const memoizedInputTypeOptions = useMemo(() => inputTypeOptions, []); - // 참조 테이블 옵션 (실제 테이블 목록에서 가져옴) + // 참조 테이블 옵션 (한글라벨 (영어명) 동시 표시) const referenceTableOptions = [ { value: "none", label: getTextFromUI(TABLE_MANAGEMENT_KEYS.LABEL_NONE, "선택 안함") }, - ...tables.map((table) => ({ value: table.tableName, label: table.displayName || table.tableName })), + ...tables.map((table) => ({ + value: table.tableName, + label: + table.displayName && table.displayName !== table.tableName + ? `${table.displayName} (${table.tableName})` + : table.tableName, + })), ]; // 공통 코드 카테고리 목록 상태 @@ -1596,6 +1602,8 @@ export default function TableManagementPage() { onIndexToggle={(columnName, checked) => handleIndexToggle(columnName, "index", checked) } + tables={tables} + referenceTableColumns={referenceTableColumns} /> )} @@ -1795,11 +1803,16 @@ export default function TableManagementPage() {

변경될 PK 컬럼:

{pendingPkColumns.length > 0 ? (
- {pendingPkColumns.map((col) => ( - - {col} - - ))} + {pendingPkColumns.map((col) => { + const colInfo = columns.find((c) => c.columnName === col); + return ( + + {colInfo?.displayName && colInfo.displayName !== col + ? `${colInfo.displayName} (${col})` + : col} + + ); + })}
) : (

PK가 모두 제거됩니다

diff --git a/frontend/components/admin/table-type/ColumnDetailPanel.tsx b/frontend/components/admin/table-type/ColumnDetailPanel.tsx index 77f5dedf..5e4e2a07 100644 --- a/frontend/components/admin/table-type/ColumnDetailPanel.tsx +++ b/frontend/components/admin/table-type/ColumnDetailPanel.tsx @@ -78,7 +78,16 @@ export function ColumnDetailPanel({ const refTableOpts = referenceTableOptions.length ? referenceTableOptions - : [{ value: "none", label: "선택 안함" }, ...tables.map((t) => ({ value: t.tableName, label: t.displayName || t.tableName }))]; + : [ + { value: "none", label: "선택 안함" }, + ...tables.map((t) => ({ + value: t.tableName, + label: + t.displayName && t.displayName !== t.tableName + ? `${t.displayName} (${t.tableName})` + : t.tableName, + })), + ]; return (
@@ -90,7 +99,11 @@ export function ColumnDetailPanel({ {typeConf.label} )} - {column.columnName} + + {column.displayName && column.displayName !== column.columnName + ? `${column.displayName} (${column.columnName})` + : column.columnName} +
@@ -245,7 +263,13 @@ export function ColumnDetailPanel({ column.referenceColumn === refCol.columnName ? "opacity-100" : "opacity-0", )} /> - {refCol.columnName} +
+ + {refCol.displayName && refCol.displayName !== refCol.columnName + ? `${refCol.displayName} (${refCol.columnName})` + : refCol.columnName} + +
))} @@ -259,12 +283,20 @@ export function ColumnDetailPanel({ {/* 참조 요약 미니맵 */} {column.referenceTable && column.referenceTable !== "none" && column.referenceColumn && (
- - {column.referenceTable} + + {(() => { + const tbl = refTableOpts.find((o) => o.value === column.referenceTable); + return tbl?.label ?? column.referenceTable; + })()} - - {column.referenceColumn} + + {(() => { + const col = refColumns.find((c) => c.columnName === column.referenceColumn); + return col?.displayName && col.displayName !== column.referenceColumn + ? `${col.displayName} (${column.referenceColumn})` + : column.referenceColumn; + })()}
)} diff --git a/frontend/components/admin/table-type/ColumnGrid.tsx b/frontend/components/admin/table-type/ColumnGrid.tsx index c03c7516..825dbd36 100644 --- a/frontend/components/admin/table-type/ColumnGrid.tsx +++ b/frontend/components/admin/table-type/ColumnGrid.tsx @@ -5,8 +5,9 @@ import { MoreHorizontal, Database, Layers, FileStack } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { cn } from "@/lib/utils"; -import type { ColumnTypeInfo } from "./types"; +import type { ColumnTypeInfo, TableInfo } from "./types"; import { INPUT_TYPE_COLORS, getColumnGroup } from "./types"; +import type { ReferenceTableColumn } from "@/lib/api/entityJoin"; export interface ColumnGridConstraints { primaryKey: { columns: string[] }; @@ -23,6 +24,9 @@ export interface ColumnGridProps { getColumnIndexState?: (columnName: string) => { isPk: boolean; hasIndex: boolean }; onPkToggle?: (columnName: string, checked: boolean) => void; onIndexToggle?: (columnName: string, checked: boolean) => void; + /** 호버 시 한글 라벨 표시용 (Badge title) */ + tables?: TableInfo[]; + referenceTableColumns?: Record; } function getIndexState( @@ -53,6 +57,8 @@ export function ColumnGrid({ getColumnIndexState: externalGetIndexState, onPkToggle, onIndexToggle, + tables, + referenceTableColumns, }: ColumnGridProps) { const getIdxState = useMemo( () => externalGetIndexState ?? ((name: string) => getIndexState(name, constraints)), @@ -136,13 +142,12 @@ export function ColumnGrid({ {/* 4px 색상바 (타입별 진한 색) */}
- {/* 라벨 + 컬럼명 */} + {/* 라벨 + 컬럼명 (한글라벨 (영어명) 동시 표시) */}
- {column.displayName || column.columnName} -
-
- {column.columnName} + {column.displayName && column.displayName !== column.columnName + ? `${column.displayName} (${column.columnName})` + : column.columnName}
@@ -150,11 +155,38 @@ export function ColumnGrid({
{column.inputType === "entity" && column.referenceTable && column.referenceTable !== "none" && ( <> - + { + const t = tables.find((tb) => tb.tableName === column.referenceTable); + return t?.displayName && t.displayName !== t.tableName + ? `${t.displayName} (${column.referenceTable})` + : column.referenceTable; + })() + : column.referenceTable + } + > {column.referenceTable} - + { + const refCols = referenceTableColumns[column.referenceTable]; + const c = refCols.find((rc) => rc.columnName === (column.referenceColumn ?? "")); + return c?.displayName && c.displayName !== c.columnName + ? `${c.displayName} (${column.referenceColumn})` + : column.referenceColumn ?? "—"; + })() + : column.referenceColumn ?? "—" + } + > {column.referenceColumn || "—"} diff --git a/frontend/components/layout/AppLayout.tsx b/frontend/components/layout/AppLayout.tsx index c80cb581..d2f13c79 100644 --- a/frontend/components/layout/AppLayout.tsx +++ b/frontend/components/layout/AppLayout.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, Suspense, useEffect } from "react"; +import { useState, Suspense, useEffect, useCallback } from "react"; import { useRouter, usePathname, useSearchParams } from "next/navigation"; import { Button } from "@/components/ui/button"; import { @@ -341,6 +341,10 @@ function AppLayoutInner({ children }: AppLayoutProps) { const currentMenus = isAdminMode ? adminMenus : userMenus; + const currentTabs = useTabStore((s) => s[s.mode].tabs); + const currentActiveTabId = useTabStore((s) => s[s.mode].activeTabId); + const activeTab = currentTabs.find((t) => t.id === currentActiveTabId); + const toggleMenu = (menuId: string) => { const newExpanded = new Set(expandedMenus); if (newExpanded.has(menuId)) { @@ -478,6 +482,26 @@ function AppLayoutInner({ children }: AppLayoutProps) { } }; + // pathname + 활성 탭 기반 활성 메뉴 판별 (탭 네비게이션에서도 사이드바 활성 표시) + const isMenuActive = useCallback( + (menu: any): boolean => { + if (pathname === menu.url) return true; + if (!activeTab) return false; + + const menuObjid = parseInt((menu.objid || menu.id)?.toString() || "0"); + + if (activeTab.type === "admin" && activeTab.adminUrl) { + return menu.url === activeTab.adminUrl; + } + if (activeTab.type === "screen") { + if (activeTab.menuObjid != null && menuObjid === activeTab.menuObjid) return true; + if (activeTab.screenId != null && menu.screenId === activeTab.screenId) return true; + } + return false; + }, + [pathname, activeTab], + ); + // 메뉴 트리 렌더링 (기존 MainLayout 스타일 적용) const renderMenu = (menu: any, level: number = 0) => { const isExpanded = expandedMenus.has(menu.id); @@ -489,8 +513,8 @@ function AppLayoutInner({ children }: AppLayoutProps) { draggable={isLeaf} onDragStart={(e) => handleMenuDragStart(e, menu)} className={`group flex min-h-[44px] cursor-pointer items-center justify-between rounded-md px-3 py-2 text-sm font-medium transition-colors duration-150 ease-in-out sm:min-h-[40px] ${ - pathname === menu.url - ? "border-primary bg-primary/8 text-primary border-l-3 font-semibold" + isMenuActive(menu) + ? "border-l-[3px] border-l-primary bg-primary/10 dark:bg-primary/15 text-primary font-semibold" : isExpanded ? "bg-accent/60 text-foreground" : "text-muted-foreground hover:bg-accent hover:text-foreground" @@ -518,8 +542,8 @@ function AppLayoutInner({ children }: AppLayoutProps) { draggable={!child.hasChildren} onDragStart={(e) => handleMenuDragStart(e, child)} className={`flex min-h-[44px] cursor-pointer items-center rounded-md px-3 py-2 text-sm transition-colors duration-150 hover:cursor-pointer sm:min-h-[40px] ${ - pathname === child.url - ? "border-primary bg-primary/8 text-primary border-l-3 font-semibold" + isMenuActive(child) + ? "border-l-[3px] border-l-primary bg-primary/10 dark:bg-primary/15 text-primary font-semibold" : "text-muted-foreground hover:bg-accent hover:text-foreground" }`} onClick={() => handleMenuClick(child)} @@ -557,6 +581,28 @@ function AppLayoutInner({ children }: AppLayoutProps) { const uiMenus = convertMenuToUI(currentMenus, user as ExtendedUserInfo); + // 활성 탭에 해당하는 메뉴가 속한 부모 메뉴 자동 확장 + useEffect(() => { + if (!activeTab || uiMenus.length === 0) return; + + const toExpand: string[] = []; + for (const menu of uiMenus) { + if (menu.hasChildren && menu.children) { + const hasActiveChild = menu.children.some((child: any) => isMenuActive(child)); + if (hasActiveChild && !expandedMenus.has(menu.id)) { + toExpand.push(menu.id); + } + } + } + if (toExpand.length > 0) { + setExpandedMenus((prev) => { + const next = new Set(prev); + toExpand.forEach((id) => next.add(id)); + return next; + }); + } + }, [activeTab, uiMenus, isMenuActive, expandedMenus]); + return (
{/* 모바일 헤더 */} diff --git a/frontend/components/table-category/CategoryColumnList.tsx b/frontend/components/table-category/CategoryColumnList.tsx index 2aed73fd..1b1bf0a5 100644 --- a/frontend/components/table-category/CategoryColumnList.tsx +++ b/frontend/components/table-category/CategoryColumnList.tsx @@ -5,6 +5,7 @@ import { apiClient } from "@/lib/api/client"; import { getCategoryValues } from "@/lib/api/tableCategoryValue"; import { ChevronRight, FolderTree, Loader2, Search, X } from "lucide-react"; import { Input } from "@/components/ui/input"; +import { cn } from "@/lib/utils"; export interface CategoryColumn { tableName: string; diff --git a/frontend/components/ui/tabs.tsx b/frontend/components/ui/tabs.tsx index 95fdc082..568d44ed 100644 --- a/frontend/components/ui/tabs.tsx +++ b/frontend/components/ui/tabs.tsx @@ -26,7 +26,7 @@ function TabsList({ className={cn( "px-3 py-1 text-sm font-medium transition-colors", activeTabIndex === 0 - ? "text-foreground border-b-2 border-primary" - : "text-muted-foreground hover:text-foreground" + ? "text-primary border-b-2 border-primary font-semibold bg-primary/5" + : "text-foreground/70 hover:text-foreground hover:bg-muted/30" )} > {componentConfig.rightPanel?.title || "기본"} @@ -3994,8 +3994,8 @@ export const SplitPanelLayoutComponent: React.FC className={cn( "px-3 py-1 text-sm font-medium transition-colors", activeTabIndex === index + 1 - ? "text-foreground border-b-2 border-primary" - : "text-muted-foreground hover:text-foreground" + ? "text-primary border-b-2 border-primary font-semibold bg-primary/5" + : "text-foreground/70 hover:text-foreground hover:bg-muted/30" )} > {tab.label || `탭 ${index + 1}`} diff --git a/frontend/lib/registry/components/v2-tabs-widget/tabs-component.tsx b/frontend/lib/registry/components/v2-tabs-widget/tabs-component.tsx index ac6b208e..03de3cc1 100644 --- a/frontend/lib/registry/components/v2-tabs-widget/tabs-component.tsx +++ b/frontend/lib/registry/components/v2-tabs-widget/tabs-component.tsx @@ -48,8 +48,8 @@ const TabsDesignEditor: React.FC<{ return cn( "px-4 py-2 text-sm font-medium cursor-pointer transition-colors", isActive - ? "bg-background border-b-2 border-primary text-primary" - : "text-muted-foreground hover:text-foreground hover:bg-muted/50" + ? "bg-primary/10 border-b-2 border-primary text-primary font-semibold" + : "text-foreground/70 hover:text-foreground hover:bg-muted/50" ); }; @@ -283,7 +283,7 @@ const TabsDesignEditor: React.FC<{ return (
{/* 탭 헤더 */} -
+
{tabs.length > 0 ? ( tabs.map((tab) => (
{/* 탭 헤더 */} -
+
{tabs.length > 0 ? ( tabs.map((tab) => (
Date: Tue, 17 Mar 2026 18:17:51 +0900 Subject: [PATCH 13/23] [agent-pipeline] pipe-20260317084014-ydap round-2 --- frontend/components/layout/AppLayout.tsx | 60 ++++++++++--------- .../SplitPanelLayoutComponent.tsx | 4 +- 2 files changed, 34 insertions(+), 30 deletions(-) diff --git a/frontend/components/layout/AppLayout.tsx b/frontend/components/layout/AppLayout.tsx index d2f13c79..f206b718 100644 --- a/frontend/components/layout/AppLayout.tsx +++ b/frontend/components/layout/AppLayout.tsx @@ -490,12 +490,12 @@ function AppLayoutInner({ children }: AppLayoutProps) { const menuObjid = parseInt((menu.objid || menu.id)?.toString() || "0"); - if (activeTab.type === "admin" && activeTab.adminUrl) { + if (activeTab?.type === "admin" && activeTab?.adminUrl) { return menu.url === activeTab.adminUrl; } - if (activeTab.type === "screen") { - if (activeTab.menuObjid != null && menuObjid === activeTab.menuObjid) return true; - if (activeTab.screenId != null && menu.screenId === activeTab.screenId) return true; + if (activeTab?.type === "screen") { + if (activeTab?.menuObjid != null && menuObjid === activeTab.menuObjid) return true; + if (activeTab?.screenId != null && menu.screenId === activeTab.screenId) return true; } return false; }, @@ -562,6 +562,34 @@ function AppLayoutInner({ children }: AppLayoutProps) { ); }; + const uiMenus = user ? convertMenuToUI(currentMenus, user as ExtendedUserInfo) : []; + + // 활성 탭에 해당하는 메뉴가 속한 부모 메뉴 자동 확장 (기존 확장 상태 유지, 추가만 함) + useEffect(() => { + if (!activeTab || uiMenus.length === 0) return; + + const toExpand: string[] = []; + for (const menu of uiMenus) { + if (menu.hasChildren && menu.children) { + const hasActiveChild = menu.children.some((child: any) => isMenuActive(child)); + if (hasActiveChild) toExpand.push(menu.id); + } + } + if (toExpand.length > 0) { + setExpandedMenus((prev) => { + const next = new Set(prev); + let changed = false; + toExpand.forEach((id) => { + if (!next.has(id)) { + next.add(id); + changed = true; + } + }); + return changed ? next : prev; + }); + } + }, [activeTab, uiMenus, isMenuActive]); + if (isPreviewMode) { return (
{children}
@@ -579,30 +607,6 @@ function AppLayoutInner({ children }: AppLayoutProps) { ); } - const uiMenus = convertMenuToUI(currentMenus, user as ExtendedUserInfo); - - // 활성 탭에 해당하는 메뉴가 속한 부모 메뉴 자동 확장 - useEffect(() => { - if (!activeTab || uiMenus.length === 0) return; - - const toExpand: string[] = []; - for (const menu of uiMenus) { - if (menu.hasChildren && menu.children) { - const hasActiveChild = menu.children.some((child: any) => isMenuActive(child)); - if (hasActiveChild && !expandedMenus.has(menu.id)) { - toExpand.push(menu.id); - } - } - } - if (toExpand.length > 0) { - setExpandedMenus((prev) => { - const next = new Set(prev); - toExpand.forEach((id) => next.add(id)); - return next; - }); - } - }, [activeTab, uiMenus, isMenuActive, expandedMenus]); - return (
{/* 모바일 헤더 */} 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 28acdbe6..4527a85f 100644 --- a/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutComponent.tsx +++ b/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutComponent.tsx @@ -21,7 +21,7 @@ import { Move, FileSpreadsheet, List, - LayoutPanelRight, + PanelRight, } from "lucide-react"; import { dataApi } from "@/lib/api/data"; import { entityJoinApi } from "@/lib/api/entityJoin"; @@ -3972,7 +3972,7 @@ export const SplitPanelLayoutComponent: React.FC >
- + {/* 탭이 없으면 제목만, 있으면 탭으로 전환 (2px primary 밑줄 인디케이터) */} {(componentConfig.rightPanel?.additionalTabs?.length || 0) > 0 ? (
From ad48b22770d0949d712991955a6f551c04e33a1c Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Tue, 17 Mar 2026 18:17:51 +0900 Subject: [PATCH 14/23] [agent-pipeline] rollback to d3acf391 --- frontend/components/layout/AppLayout.tsx | 60 +++++++++---------- .../SplitPanelLayoutComponent.tsx | 4 +- 2 files changed, 30 insertions(+), 34 deletions(-) diff --git a/frontend/components/layout/AppLayout.tsx b/frontend/components/layout/AppLayout.tsx index f206b718..d2f13c79 100644 --- a/frontend/components/layout/AppLayout.tsx +++ b/frontend/components/layout/AppLayout.tsx @@ -490,12 +490,12 @@ function AppLayoutInner({ children }: AppLayoutProps) { const menuObjid = parseInt((menu.objid || menu.id)?.toString() || "0"); - if (activeTab?.type === "admin" && activeTab?.adminUrl) { + if (activeTab.type === "admin" && activeTab.adminUrl) { return menu.url === activeTab.adminUrl; } - if (activeTab?.type === "screen") { - if (activeTab?.menuObjid != null && menuObjid === activeTab.menuObjid) return true; - if (activeTab?.screenId != null && menu.screenId === activeTab.screenId) return true; + if (activeTab.type === "screen") { + if (activeTab.menuObjid != null && menuObjid === activeTab.menuObjid) return true; + if (activeTab.screenId != null && menu.screenId === activeTab.screenId) return true; } return false; }, @@ -562,34 +562,6 @@ function AppLayoutInner({ children }: AppLayoutProps) { ); }; - const uiMenus = user ? convertMenuToUI(currentMenus, user as ExtendedUserInfo) : []; - - // 활성 탭에 해당하는 메뉴가 속한 부모 메뉴 자동 확장 (기존 확장 상태 유지, 추가만 함) - useEffect(() => { - if (!activeTab || uiMenus.length === 0) return; - - const toExpand: string[] = []; - for (const menu of uiMenus) { - if (menu.hasChildren && menu.children) { - const hasActiveChild = menu.children.some((child: any) => isMenuActive(child)); - if (hasActiveChild) toExpand.push(menu.id); - } - } - if (toExpand.length > 0) { - setExpandedMenus((prev) => { - const next = new Set(prev); - let changed = false; - toExpand.forEach((id) => { - if (!next.has(id)) { - next.add(id); - changed = true; - } - }); - return changed ? next : prev; - }); - } - }, [activeTab, uiMenus, isMenuActive]); - if (isPreviewMode) { return (
{children}
@@ -607,6 +579,30 @@ function AppLayoutInner({ children }: AppLayoutProps) { ); } + const uiMenus = convertMenuToUI(currentMenus, user as ExtendedUserInfo); + + // 활성 탭에 해당하는 메뉴가 속한 부모 메뉴 자동 확장 + useEffect(() => { + if (!activeTab || uiMenus.length === 0) return; + + const toExpand: string[] = []; + for (const menu of uiMenus) { + if (menu.hasChildren && menu.children) { + const hasActiveChild = menu.children.some((child: any) => isMenuActive(child)); + if (hasActiveChild && !expandedMenus.has(menu.id)) { + toExpand.push(menu.id); + } + } + } + if (toExpand.length > 0) { + setExpandedMenus((prev) => { + const next = new Set(prev); + toExpand.forEach((id) => next.add(id)); + return next; + }); + } + }, [activeTab, uiMenus, isMenuActive, expandedMenus]); + return (
{/* 모바일 헤더 */} 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 4527a85f..28acdbe6 100644 --- a/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutComponent.tsx +++ b/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutComponent.tsx @@ -21,7 +21,7 @@ import { Move, FileSpreadsheet, List, - PanelRight, + LayoutPanelRight, } from "lucide-react"; import { dataApi } from "@/lib/api/data"; import { entityJoinApi } from "@/lib/api/entityJoin"; @@ -3972,7 +3972,7 @@ export const SplitPanelLayoutComponent: React.FC >
- + {/* 탭이 없으면 제목만, 있으면 탭으로 전환 (2px primary 밑줄 인디케이터) */} {(componentConfig.rightPanel?.additionalTabs?.length || 0) > 0 ? (
From cfd7ee9fcedb147b3d21e4aa09b257129d6a73ad Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Tue, 17 Mar 2026 18:25:36 +0900 Subject: [PATCH 15/23] [agent-pipeline] pipe-20260317084014-ydap round-3 --- frontend/hooks/useLogin.ts | 62 +++++++++++--------------------------- 1 file changed, 17 insertions(+), 45 deletions(-) diff --git a/frontend/hooks/useLogin.ts b/frontend/hooks/useLogin.ts index bd0cf9a2..01231441 100644 --- a/frontend/hooks/useLogin.ts +++ b/frontend/hooks/useLogin.ts @@ -2,12 +2,13 @@ import { useState, useEffect, useCallback } from "react"; import { useRouter } from "next/navigation"; -import { LoginFormData, LoginResponse } from "@/types/auth"; +import { LoginFormData } from "@/types/auth"; import { AUTH_CONFIG, FORM_VALIDATION } from "@/constants/auth"; -import { API_BASE_URL } from "@/lib/api/client"; +import { apiCall } from "@/lib/api/client"; /** * 로그인 관련 비즈니스 로직을 관리하는 커스텀 훅 + * API 호출은 lib/api/client의 apiCall(Axios) 사용 (fetch 직접 사용 금지) */ export const useLogin = () => { const router = useRouter(); @@ -73,67 +74,34 @@ export const useLogin = () => { }, [formData]); /** - * API 호출 공통 함수 - */ - const apiCall = useCallback(async (endpoint: string, options: RequestInit = {}): Promise => { - // 로컬 스토리지에서 토큰 가져오기 - const token = localStorage.getItem("authToken"); - - const response = await fetch(`${API_BASE_URL}${endpoint}`, { - credentials: "include", - headers: { - "Content-Type": "application/json", - Accept: "application/json", - ...(token && { Authorization: `Bearer ${token}` }), - ...options.headers, - }, - ...options, - }); - - const result = await response.json(); - return result; - }, []); - - /** - * 기존 인증 상태 확인 + * 기존 인증 상태 확인 (apiCall 사용) */ const checkExistingAuth = useCallback(async () => { try { - // 로컬 스토리지에서 토큰 확인 const token = localStorage.getItem("authToken"); - if (!token) { - // 토큰이 없으면 로그인 페이지 유지 - return; - } + if (!token) return; - // 토큰이 있으면 API 호출로 유효성 확인 - const result = await apiCall(AUTH_CONFIG.ENDPOINTS.STATUS); + const result = await apiCall<{ isAuthenticated?: boolean }>("GET", AUTH_CONFIG.ENDPOINTS.STATUS); - // 백엔드가 isAuthenticated 필드를 반환함 if (result.success && result.data?.isAuthenticated) { - // 이미 로그인된 경우 메인으로 리다이렉트 router.push(AUTH_CONFIG.ROUTES.MAIN); } else { - // 토큰이 유효하지 않으면 제거 localStorage.removeItem("authToken"); document.cookie = "authToken=; path=/; max-age=0; SameSite=Lax"; } - } catch (error) { - // 에러가 발생하면 토큰 제거 + } catch { localStorage.removeItem("authToken"); document.cookie = "authToken=; path=/; max-age=0; SameSite=Lax"; - console.debug("기존 인증 체크 중 오류 (정상):", error); } - }, [apiCall, router]); + }, [router]); /** - * 로그인 처리 + * 로그인 처리 (apiCall 사용 - Axios 기반, fetch 미사용) */ const handleLogin = useCallback( async (e: React.FormEvent) => { e.preventDefault(); - // 입력값 검증 const validationError = validateForm(); if (validationError) { setError(validationError); @@ -144,9 +112,13 @@ export const useLogin = () => { setError(""); try { - const result = await apiCall(AUTH_CONFIG.ENDPOINTS.LOGIN, { - method: "POST", - body: JSON.stringify(formData), + const result = await apiCall<{ + token?: string; + firstMenuPath?: string; + popLandingPath?: string; + }>("POST", AUTH_CONFIG.ENDPOINTS.LOGIN, { + userId: formData.userId, + password: formData.password, }); if (result.success && result.data?.token) { @@ -185,7 +157,7 @@ export const useLogin = () => { setIsLoading(false); } }, - [formData, validateForm, apiCall, router, isPopMode], + [formData, validateForm, router, isPopMode], ); // 컴포넌트 마운트 시 기존 인증 상태 확인 From b293d184bb23eaceab6b446f7b2fcb06f84cccdc Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Tue, 17 Mar 2026 21:50:37 +0900 Subject: [PATCH 16/23] 11 --- .../admin/table-type/ColumnDetailPanel.tsx | 99 ++++++++++++------- frontend/components/layout/AppLayout.tsx | 24 ++--- frontend/components/layout/TabBar.tsx | 4 +- .../V2SplitPanelLayoutConfigPanel.tsx | 14 ++- .../SplitPanelLayoutComponent.tsx | 91 ++++++++--------- .../v2-table-list/SingleTableWithSticky.tsx | 27 ++--- .../v2-table-list/TableListComponent.tsx | 97 ++++++++++++------ .../v2-tabs-widget/tabs-component.tsx | 4 +- 8 files changed, 219 insertions(+), 141 deletions(-) diff --git a/frontend/components/admin/table-type/ColumnDetailPanel.tsx b/frontend/components/admin/table-type/ColumnDetailPanel.tsx index 5e4e2a07..c462d0ff 100644 --- a/frontend/components/admin/table-type/ColumnDetailPanel.tsx +++ b/frontend/components/admin/table-type/ColumnDetailPanel.tsx @@ -76,18 +76,34 @@ export function ColumnDetailPanel({ if (!column) return null; - const refTableOpts = referenceTableOptions.length - ? referenceTableOptions - : [ - { value: "none", label: "선택 안함" }, - ...tables.map((t) => ({ - value: t.tableName, - label: - t.displayName && t.displayName !== t.tableName - ? `${t.displayName} (${t.tableName})` - : t.tableName, - })), - ]; + const refTableOpts = useMemo(() => { + const hasKorean = (s: string) => /[가-힣]/.test(s); + const raw = referenceTableOptions.length + ? [...referenceTableOptions] + : [ + { value: "none", label: "없음" }, + ...tables.map((t) => ({ + value: t.tableName, + label: + t.displayName && t.displayName !== t.tableName + ? `${t.displayName} (${t.tableName})` + : t.tableName, + })), + ]; + + const noneOpt = raw.find((o) => o.value === "none"); + const rest = raw.filter((o) => o.value !== "none"); + + rest.sort((a, b) => { + const aK = hasKorean(a.label); + const bK = hasKorean(b.label); + if (aK && !bK) return -1; + if (!aK && bK) return 1; + return a.label.localeCompare(b.label, "ko"); + }); + + return noneOpt ? [noneOpt, ...rest] : rest; + }, [referenceTableOptions, tables]); return (
@@ -183,23 +199,33 @@ export function ColumnDetailPanel({ 테이블을 찾을 수 없습니다. - {refTableOpts.map((opt) => ( - { - onColumnChange("referenceTable", opt.value === "none" ? undefined : opt.value); - if (opt.value !== "none") onLoadReferenceColumns?.(opt.value); - setEntityTableOpen(false); - }} - className="text-xs" - > - - {opt.label} - - ))} + {refTableOpts.map((opt) => { + const hasKorean = opt.value !== "none" && opt.label !== opt.value && !opt.label.startsWith(opt.value); + return ( + { + onColumnChange("referenceTable", opt.value === "none" ? undefined : opt.value); + if (opt.value !== "none") onLoadReferenceColumns?.(opt.value); + setEntityTableOpen(false); + }} + className="text-xs" + > + + {hasKorean ? ( +
+ {opt.label.replace(` (${opt.value})`, "")} + {opt.value} +
+ ) : ( + opt.label + )} +
+ ); + })}
@@ -263,13 +289,14 @@ export function ColumnDetailPanel({ column.referenceColumn === refCol.columnName ? "opacity-100" : "opacity-0", )} /> -
- - {refCol.displayName && refCol.displayName !== refCol.columnName - ? `${refCol.displayName} (${refCol.columnName})` - : refCol.columnName} - -
+ {refCol.displayName && refCol.displayName !== refCol.columnName ? ( +
+ {refCol.displayName} + {refCol.columnName} +
+ ) : ( + {refCol.columnName} + )} ))} diff --git a/frontend/components/layout/AppLayout.tsx b/frontend/components/layout/AppLayout.tsx index d2f13c79..2014d535 100644 --- a/frontend/components/layout/AppLayout.tsx +++ b/frontend/components/layout/AppLayout.tsx @@ -568,18 +568,7 @@ function AppLayoutInner({ children }: AppLayoutProps) { ); } - if (!user) { - return ( -
-
-
-

로딩중...

-
-
- ); - } - - const uiMenus = convertMenuToUI(currentMenus, user as ExtendedUserInfo); + const uiMenus = user ? convertMenuToUI(currentMenus, user as ExtendedUserInfo) : []; // 활성 탭에 해당하는 메뉴가 속한 부모 메뉴 자동 확장 useEffect(() => { @@ -603,6 +592,17 @@ function AppLayoutInner({ children }: AppLayoutProps) { } }, [activeTab, uiMenus, isMenuActive, expandedMenus]); + if (!user) { + return ( +
+
+
+

로딩중...

+
+
+ ); + } + return (
{/* 모바일 헤더 */} diff --git a/frontend/components/layout/TabBar.tsx b/frontend/components/layout/TabBar.tsx index e86ada2e..1ac5144e 100644 --- a/frontend/components/layout/TabBar.tsx +++ b/frontend/components/layout/TabBar.tsx @@ -493,8 +493,8 @@ export function TabBar() { className={cn( "group relative flex h-7 shrink-0 cursor-pointer items-center gap-0.5 rounded-t-md border border-b-0 px-3 select-none", isActive - ? "text-foreground z-10 -mb-px h-[30px] bg-white" - : "bg-muted/50 text-muted-foreground hover:bg-muted hover:text-foreground border-transparent", + ? "text-primary z-10 -mb-px h-[30px] bg-primary/15 dark:bg-primary/20 border-primary/40 border-t-[3px] border-t-primary font-semibold" + : "bg-transparent text-muted-foreground hover:bg-muted/50 hover:text-foreground border-transparent", )} style={{ width: TAB_WIDTH, diff --git a/frontend/components/v2/config-panels/V2SplitPanelLayoutConfigPanel.tsx b/frontend/components/v2/config-panels/V2SplitPanelLayoutConfigPanel.tsx index ae5679b6..97ff71a3 100644 --- a/frontend/components/v2/config-panels/V2SplitPanelLayoutConfigPanel.tsx +++ b/frontend/components/v2/config-panels/V2SplitPanelLayoutConfigPanel.tsx @@ -1552,16 +1552,22 @@ export const V2SplitPanelLayoutConfigPanel: React.FC< /> - updateRightPanel({ showEdit: checked }) + updateRightPanel({ + showEdit: checked, + editButton: { ...config.rightPanel?.editButton!, enabled: checked }, + }) } /> - updateRightPanel({ showDelete: checked }) + updateRightPanel({ + showDelete: checked, + deleteButton: { ...config.rightPanel?.deleteButton!, enabled: checked }, + }) } />
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 28acdbe6..0c585587 100644 --- a/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutComponent.tsx +++ b/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutComponent.tsx @@ -21,7 +21,7 @@ import { Move, FileSpreadsheet, List, - LayoutPanelRight, + PanelRight, } from "lucide-react"; import { dataApi } from "@/lib/api/data"; import { entityJoinApi } from "@/lib/api/entityJoin"; @@ -3524,7 +3524,7 @@ export const SplitPanelLayoutComponent: React.FC {columnsToShow.map((col, idx) => (
+
+
); })} {hasTabActions && ( - 작업작업
+ {col.type === "progress" ? renderProgressCell(col, item, selectedLeftItem) : formatCellValue( @@ -4256,7 +4256,7 @@ export const SplitPanelLayoutComponent: React.FC ); })} {hasTabActions && ( - 작업작업
+ {col.type === "progress" ? renderProgressCell(col, item, selectedLeftItem) : formatCellValue( @@ -4670,7 +4670,7 @@ export const SplitPanelLayoutComponent: React.FC + {(() => { + const rightEditVisible = (componentConfig.rightPanel?.showEdit ?? componentConfig.rightPanel?.editButton?.enabled) !== false; + const rightDeleteVisible = (componentConfig.rightPanel?.showDelete ?? componentConfig.rightPanel?.deleteButton?.enabled) !== false; + return !isDesignMode && (rightEditVisible || rightDeleteVisible) ? ( + 작업
{col.type === "progress" @@ -4722,12 +4725,10 @@ export const SplitPanelLayoutComponent: React.FC + {!isDesignMode && (rightEditVisible || rightDeleteVisible) && ( +
- {(componentConfig.rightPanel?.editButton?.enabled ?? true) && ( + {rightEditVisible && ( )} - {(componentConfig.rightPanel?.deleteButton?.enabled ?? true) && ( + {rightDeleteVisible && (
{col.label} 작업작업
+ {formatCellValue( col.name, getEntityJoinValue(item, col.name), @@ -4865,7 +4866,7 @@ export const SplitPanelLayoutComponent: React.FC +
{hasEditButton && (
- {/* 🆕 배치 편집 툴바 */} + {/* 필터 칩 바 */} + {filterGroups.length > 0 && filterGroups.some(g => g.conditions.some(c => c.column && c.value)) && ( +
+ {filterGroups.flatMap(group => + group.conditions + .filter(c => c.column && c.value) + .map(condition => { + const label = columnLabels[condition.column] || condition.column; + const opLabel = condition.operator === "equals" ? "=" : condition.operator === "contains" ? "⊃" : condition.operator === "notEquals" ? "≠" : condition.operator === "startsWith" ? "^" : condition.operator === "endsWith" ? "$" : condition.operator === "greaterThan" ? ">" : condition.operator === "lessThan" ? "<" : condition.operator; + return ( + + {label} {opLabel} {condition.value} + + + ); + }) + )} + +
+ )} + + {/* 배치 편집 툴바 */} {(editMode === "batch" || pendingChanges.size > 0) && (
@@ -5826,9 +5861,9 @@ export const TableListComponent: React.FC = ({
= ({
@@ -4120,7 +4153,7 @@ export const SplitPanelLayoutComponent: React.FC ); @@ -4158,7 +4192,7 @@ export const SplitPanelLayoutComponent: React.FC toggleRightItemExpansion(`tab_${activeTabIndex}_${tabItemId}`)} > @@ -4243,7 +4277,7 @@ export const SplitPanelLayoutComponent: React.FC // showInSummary가 false가 아닌 것만 메인 테이블에 표시 const listSummaryColumns = tabColumns.filter((col: any) => col.showInSummary !== false); const listTabIndex = activeTabIndex - 1; - const canDragListTabColumns = isDesignMode && listSummaryColumns.length > 0 && !!onUpdateComponent; + const canDragListTabColumns = listSummaryColumns.length > 0; return (
onDragEnd={handleRightColumnDragEnd} onDrop={(e) => canDragTabColumns && handleRightColumnDrop(e, idx, tabIndex)} > + {canDragTabColumns && } {col.label || col.name}
@@ -4256,7 +4290,7 @@ export const SplitPanelLayoutComponent: React.FC ); @@ -4293,7 +4328,7 @@ export const SplitPanelLayoutComponent: React.FC toggleRightItemExpansion(`tab_${activeTabIndex}_${tabItemId}`)} > @@ -4646,6 +4681,14 @@ export const SplitPanelLayoutComponent: React.FC })); } + // 런타임 컬럼 순서 적용 + if (!isDesignMode && runtimeColumnOrder["main"]) { + const keyColCount = columnsToShow.filter((c: any) => c._isKeyColumn).length; + const keyCols = columnsToShow.slice(0, keyColCount); + const dataCols = columnsToShow.slice(keyColCount); + columnsToShow = [...keyCols, ...applyRuntimeOrder(dataCols, "main")]; + } + // 컬럼 너비 합계 계산 (작업 컬럼 제외, 100% 초과 시 스크롤) const rightTotalColWidth = columnsToShow.reduce((sum, col) => { const w = col.width && col.width <= 100 ? col.width : 0; @@ -4653,7 +4696,7 @@ export const SplitPanelLayoutComponent: React.FC }, 0); const rightConfigColumnStart = columnsToShow.filter((c: any) => c._isKeyColumn).length; - const canDragRightColumns = isDesignMode && displayColumns.length > 0 && !!onUpdateComponent; + const canDragRightColumns = displayColumns.length > 0; return (
@@ -4670,7 +4713,7 @@ export const SplitPanelLayoutComponent: React.FC
); @@ -4707,7 +4751,7 @@ export const SplitPanelLayoutComponent: React.FC const rightDeleteVisible = (componentConfig.rightPanel?.showDelete ?? componentConfig.rightPanel?.deleteButton?.enabled) !== false; return ( - + {columnsToShow.map((col, colIdx) => ( toggleRightItemExpansion(itemId)} > diff --git a/frontend/lib/registry/components/v2-table-list/SingleTableWithSticky.tsx b/frontend/lib/registry/components/v2-table-list/SingleTableWithSticky.tsx index 00daa8eb..c264dec2 100644 --- a/frontend/lib/registry/components/v2-table-list/SingleTableWithSticky.tsx +++ b/frontend/lib/registry/components/v2-table-list/SingleTableWithSticky.tsx @@ -110,7 +110,7 @@ export const SingleTableWithSticky: React.FC = ({ > {actualColumns.map((column, colIndex) => { @@ -136,7 +136,7 @@ export const SingleTableWithSticky: React.FC = ({ ? "h-9 border-0 px-3 py-1.5 text-center align-middle sm:px-4 sm:py-2" : "text-muted-foreground hover:text-foreground h-9 cursor-pointer border-0 px-3 py-1.5 text-left align-middle text-[10px] font-bold uppercase tracking-[0.04em] whitespace-nowrap transition-all duration-200 select-none sm:px-4 sm:py-2 sm:text-xs", `text-${column.align}`, - column.sortable && "hover:bg-muted/70", + column.sortable && "hover:bg-muted/50", // 고정 컬럼 스타일 column.fixed === "left" && "border-border bg-background sticky z-40 border-r shadow-sm", column.fixed === "right" && "border-border bg-background sticky z-40 border-l shadow-sm", @@ -151,7 +151,7 @@ export const SingleTableWithSticky: React.FC = ({ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", // 텍스트 줄바꿈 방지 - backgroundColor: "hsl(var(--muted) / 0.8)", + backgroundColor: "hsl(var(--muted) / 0.4)", // sticky 위치 설정 ...(column.fixed === "left" && { left: leftFixedWidth }), ...(column.fixed === "right" && { right: rightFixedWidth }), @@ -230,7 +230,7 @@ export const SingleTableWithSticky: React.FC = ({ key={`row-${index}`} className={cn( "cursor-pointer border-b border-border/50 transition-[background] duration-75", - index % 2 === 0 ? "bg-background" : "bg-muted/70", + index % 2 === 0 ? "bg-background" : "bg-muted/20", tableConfig.tableStyle?.hoverEffect !== false && "hover:bg-accent", )} onClick={(e) => handleRowClick?.(row, index, e)} diff --git a/frontend/lib/registry/components/v2-table-list/TableListComponent.tsx b/frontend/lib/registry/components/v2-table-list/TableListComponent.tsx index f9e39810..7331c7bd 100644 --- a/frontend/lib/registry/components/v2-table-list/TableListComponent.tsx +++ b/frontend/lib/registry/components/v2-table-list/TableListComponent.tsx @@ -5819,8 +5819,8 @@ export const TableListComponent: React.FC = ({ {/* 🆕 Multi-Level Headers (Column Bands) */} {columnBandsInfo?.hasBands && ( {visibleColumns.map((column, colIdx) => { // 이 컬럼이 속한 band 찾기 @@ -5863,7 +5863,7 @@ export const TableListComponent: React.FC = ({ {visibleColumns.map((column, columnIndex) => { @@ -5895,7 +5895,7 @@ export const TableListComponent: React.FC = ({ column.columnName === "__checkbox__" ? "px-0 py-1" : "px-3 py-2", column.sortable !== false && column.columnName !== "__checkbox__" && - "hover:text-foreground hover:bg-muted/70 cursor-pointer transition-colors", + "hover:text-foreground hover:bg-muted/50 cursor-pointer transition-colors", sortColumn === column.columnName && "!text-primary", isFrozen && "sticky z-40 shadow-[2px_0_4px_rgba(0,0,0,0.1)]", // 🆕 Column Reordering 스타일 @@ -5916,7 +5916,7 @@ export const TableListComponent: React.FC = ({ minWidth: column.columnName === "__checkbox__" ? "48px" : undefined, maxWidth: column.columnName === "__checkbox__" ? "48px" : undefined, userSelect: "none", - backgroundColor: "hsl(var(--muted) / 0.8)", + backgroundColor: "hsl(var(--muted) / 0.4)", ...(isFrozen && { left: `${leftPosition}px` }), }} // 🆕 Column Reordering 이벤트 @@ -6167,7 +6167,7 @@ export const TableListComponent: React.FC = ({ key={index} className={cn( "hover:bg-accent cursor-pointer border-b border-border/50 transition-[background] duration-75", - index % 2 === 0 ? "bg-background" : "bg-muted/70", + index % 2 === 0 ? "bg-background" : "bg-muted/20", )} onClick={(e) => handleRowClick(row, index, e)} > @@ -6306,9 +6306,8 @@ export const TableListComponent: React.FC = ({ key={index} className={cn( "hover:bg-accent cursor-pointer border-b border-border/50 transition-[background] duration-75", - index % 2 === 0 ? "bg-background" : "bg-muted/70", - isRowSelected && "!bg-primary/15 hover:!bg-primary/20", - isRowSelected && "[&_td]:!border-b-primary/30", + index % 2 === 0 ? "bg-background" : "bg-muted/20", + isRowSelected && "!bg-primary/10 hover:!bg-primary/15", isRowFocused && "ring-primary/50 ring-1 ring-inset", isDragEnabled && "cursor-grab active:cursor-grabbing", isDragging && "bg-muted opacity-50", @@ -6566,7 +6565,7 @@ export const TableListComponent: React.FC = ({ : undefined, ...(isFrozen && { left: `${leftPosition}px`, - backgroundColor: "hsl(var(--muted) / 0.8)", + backgroundColor: "hsl(var(--muted) / 0.4)", }), }} > From 12d4d2a8b17283829d2c670e50cf293feeaf6359 Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Tue, 17 Mar 2026 22:06:13 +0900 Subject: [PATCH 18/23] 11 --- .../SplitPanelLayoutComponent.tsx | 63 ++++++++++++------- 1 file changed, 42 insertions(+), 21 deletions(-) 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 40f00e1a..0b2002f9 100644 --- a/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutComponent.tsx +++ b/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutComponent.tsx @@ -318,7 +318,7 @@ export const SplitPanelLayoutComponent: React.FC const [rightDraggedColumnIndex, setRightDraggedColumnIndex] = useState(null); const [rightDropTargetColumnIndex, setRightDropTargetColumnIndex] = useState(null); const [rightDragSource, setRightDragSource] = useState<"main" | number | null>(null); - const [runtimeColumnOrder, setRuntimeColumnOrder] = useState>({}); + const [runtimeColumnOrder, setRuntimeColumnOrder] = useState>({}); // 데이터 상태 const [leftData, setLeftData] = useState([]); @@ -2597,17 +2597,33 @@ export const SplitPanelLayoutComponent: React.FC }); } } else { - // 런타임 모드: 로컬 상태로 순서 변경 + // 런타임 모드: 컬럼 이름 순서로 로컬 상태 관리 const key = String(source); - setRuntimeColumnOrder((prev) => { - const existing = prev[key]; - const maxLen = 100; - const order = existing || Array.from({ length: maxLen }, (_, i) => i); - const reordered = [...order]; - const [removed] = reordered.splice(fromIdx, 1); - reordered.splice(targetIndex, 0, removed); - return { ...prev, [key]: reordered }; - }); + const rightPanel = componentConfig.rightPanel || {}; + let colNames: string[] = []; + + if (source === "main") { + const allColumns = rightPanel.columns || []; + colNames = allColumns.filter((c: any) => c.showInSummary !== false).map((c: any) => c.name); + } else if (typeof source === "number") { + const tabs = rightPanel.additionalTabs || []; + const tabConfig = tabs[source]; + if (tabConfig?.columns) { + colNames = tabConfig.columns.filter((c: any) => c.showInSummary !== false).map((c: any) => c.name); + } + } + + if (colNames.length > 0) { + setRuntimeColumnOrder((prev) => { + const currentOrder = prev[key] || colNames; + const reordered = [...currentOrder]; + if (fromIdx >= 0 && fromIdx < reordered.length && targetIndex >= 0 && targetIndex < reordered.length) { + const [removed] = reordered.splice(fromIdx, 1); + reordered.splice(targetIndex, 0, removed); + } + return { ...prev, [key]: reordered }; + }); + } } handleRightColumnDragEnd(); }, @@ -2622,21 +2638,26 @@ export const SplitPanelLayoutComponent: React.FC ], ); - // 런타임 컬럼 순서 적용 헬퍼 + // 런타임 컬럼 순서 적용 헬퍼 (이름 기반) const applyRuntimeOrder = useCallback( - (columns: T[], source: "main" | number): T[] => { + (columns: any[], source: "main" | number): any[] => { const key = String(source); const order = runtimeColumnOrder[key]; - if (!order) return columns; - const result: T[] = []; - for (const idx of order) { - if (idx < columns.length) result.push(columns[idx]); + if (!order || order.length === 0) return columns; + const colMap = new Map(columns.map((col) => [col.name, col])); + const result: any[] = []; + for (const name of order) { + const col = colMap.get(name); + if (col) { + result.push(col); + colMap.delete(name); + } } - // order에 없는 나머지 컬럼 추가 - for (let i = 0; i < columns.length; i++) { - if (!order.includes(i)) result.push(columns[i]); + // order에 없는 나머지 컬럼 뒤에 추가 + for (const col of colMap.values()) { + result.push(col); } - return result.length > 0 ? result : columns; + return result; }, [runtimeColumnOrder], ); From f36638e53eafd3d2e1c4c0e7b1dc9fa39730833b Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Tue, 17 Mar 2026 22:13:15 +0900 Subject: [PATCH 19/23] 22 --- .../v2-split-panel-layout/SplitPanelLayoutComponent.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 0b2002f9..8467154a 100644 --- a/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutComponent.tsx +++ b/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutComponent.tsx @@ -2547,7 +2547,7 @@ export const SplitPanelLayoutComponent: React.FC return; } - if (onUpdateComponent) { + if (isDesignMode && onUpdateComponent) { // 디자인 모드: config에 영구 저장 const rightPanel = componentConfig.rightPanel || {}; if (source === "main") { @@ -2633,6 +2633,7 @@ export const SplitPanelLayoutComponent: React.FC componentConfig, component, onUpdateComponent, + isDesignMode, handleRightColumnDragEnd, setRuntimeColumnOrder, ], From ba8a2fec2b1b2bb5bcc7f8b3aa51d31173b765e9 Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Tue, 17 Mar 2026 22:24:47 +0900 Subject: [PATCH 20/23] Refactor RealtimePreviewDynamic and ResponsiveGridRenderer components to enhance rendering logic using CSS scale for improved layout consistency. Update SplitPanelLayoutComponent to handle drag events more effectively by passing the event object. This ensures better user interaction during column dragging. --- .../screen/RealtimePreviewDynamic.tsx | 4 +- .../screen/ResponsiveGridRenderer.tsx | 64 +++++++++++-------- .../SplitPanelLayoutComponent.tsx | 10 +-- 3 files changed, 46 insertions(+), 32 deletions(-) diff --git a/frontend/components/screen/RealtimePreviewDynamic.tsx b/frontend/components/screen/RealtimePreviewDynamic.tsx index 14314a61..6330b8d4 100644 --- a/frontend/components/screen/RealtimePreviewDynamic.tsx +++ b/frontend/components/screen/RealtimePreviewDynamic.tsx @@ -592,10 +592,10 @@ const RealtimePreviewDynamicComponent: React.FC = ({ isResizing ? "none" : isOnSplitPanel ? (isDraggingSplitPanel ? "none" : "left 0.15s ease-out, width 0.15s ease-out") : undefined, } : { - // 런타임 모드: 부모(ResponsiveGridRenderer)가 위치/너비 관리 + // 런타임 모드: CSS scale 기반 - 캔버스 픽셀 크기 그대로 사용, 부모가 scale()로 축소 ...safeComponentStyle, width: "100%", - height: displayHeight, + height: "100%", position: "relative" as const, }; diff --git a/frontend/components/screen/ResponsiveGridRenderer.tsx b/frontend/components/screen/ResponsiveGridRenderer.tsx index 1322ee99..d90a0667 100644 --- a/frontend/components/screen/ResponsiveGridRenderer.tsx +++ b/frontend/components/screen/ResponsiveGridRenderer.tsx @@ -23,8 +23,9 @@ function getComponentTypeId(component: ComponentData): string { } /** - * 디자이너 절대좌표를 캔버스 대비 비율로 변환하여 렌더링. - * 화면이 줄어들면 비율에 맞게 축소, 늘어나면 확대. + * CSS transform scale 기반 렌더링. + * 디자이너와 동일하게 캔버스 해상도(px)로 레이아웃 후 CSS scale로 축소/확대. + * 텍스트, 패딩, 버튼 등 모든 요소가 균일하게 스케일링되어 WYSIWYG 보장. */ function ProportionalRenderer({ components, @@ -47,7 +48,7 @@ function ProportionalRenderer({ }, []); const topLevel = components.filter((c) => !c.parentId); - const ratio = containerW > 0 ? containerW / canvasWidth : 1; + const scale = containerW > 0 ? containerW / canvasWidth : 1; const maxBottom = topLevel.reduce((max, c) => { const bottom = c.position.y + (c.size?.height || 40); @@ -58,30 +59,41 @@ function ProportionalRenderer({
0 ? `${maxBottom * ratio}px` : "200px" }} + className="bg-background w-full overflow-hidden" + style={{ height: containerW > 0 ? `${maxBottom * scale}px` : "200px" }} > - {containerW > 0 && - topLevel.map((component) => { - const typeId = getComponentTypeId(component); - return ( -
- {renderComponent(component)} -
- ); - })} + {containerW > 0 && ( +
+ {topLevel.map((component) => { + const typeId = getComponentTypeId(component); + return ( +
+ {renderComponent(component)} +
+ ); + })} +
+ )}
); } 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 8467154a..fc244a3d 100644 --- a/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutComponent.tsx +++ b/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutComponent.tsx @@ -2522,9 +2522,11 @@ export const SplitPanelLayoutComponent: React.FC // 우측 패널 컬럼 헤더 드래그 (디자인 모드에서 컬럼 순서 변경) const handleRightColumnDragStart = useCallback( - (columnIndex: number, source: "main" | number) => { + (e: React.DragEvent, columnIndex: number, source: "main" | number) => { setRightDraggedColumnIndex(columnIndex); setRightDragSource(source); + e.dataTransfer.effectAllowed = "move"; + e.dataTransfer.setData("text/plain", `col-${source}-${columnIndex}`); }, [], ); @@ -4181,7 +4183,7 @@ export const SplitPanelLayoutComponent: React.FC isDragging && "opacity-50", )} draggable={canDragTabColumns} - onDragStart={() => canDragTabColumns && handleRightColumnDragStart(idx, tabIndex)} + onDragStart={(e) => canDragTabColumns && handleRightColumnDragStart(e, idx, tabIndex)} onDragOver={(e) => canDragTabColumns && handleRightColumnDragOver(e, idx)} onDragEnd={handleRightColumnDragEnd} onDrop={(e) => canDragTabColumns && handleRightColumnDrop(e, idx, tabIndex)} @@ -4318,7 +4320,7 @@ export const SplitPanelLayoutComponent: React.FC isDragging && "opacity-50", )} draggable={canDragListTabColumns} - onDragStart={() => canDragListTabColumns && handleRightColumnDragStart(idx, listTabIndex)} + onDragStart={(e) => canDragListTabColumns && handleRightColumnDragStart(e, idx, listTabIndex)} onDragOver={(e) => canDragListTabColumns && handleRightColumnDragOver(e, idx)} onDragEnd={handleRightColumnDragEnd} onDrop={(e) => canDragListTabColumns && handleRightColumnDrop(e, idx, listTabIndex)} @@ -4745,7 +4747,7 @@ export const SplitPanelLayoutComponent: React.FC textAlign: col.align || "left", }} draggable={isDraggable} - onDragStart={() => isDraggable && handleRightColumnDragStart(configColIndex, "main")} + onDragStart={(e) => isDraggable && handleRightColumnDragStart(e, configColIndex, "main")} onDragOver={(e) => isDraggable && handleRightColumnDragOver(e, configColIndex)} onDragEnd={handleRightColumnDragEnd} onDrop={(e) => isDraggable && handleRightColumnDrop(e, configColIndex, "main")} From c63eaf8434c093dec52a355dc6438efcc91b81f3 Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Tue, 17 Mar 2026 22:49:42 +0900 Subject: [PATCH 21/23] 123123 --- frontend/components/admin/CompanySwitcher.tsx | 2 + .../screen/RealtimePreviewDynamic.tsx | 25 ++-- .../screen/ResponsiveGridRenderer.tsx | 64 +++++----- .../ButtonPrimaryComponent.tsx | 2 +- .../SplitPanelLayoutComponent.tsx | 110 +++++++++++++++--- 5 files changed, 142 insertions(+), 61 deletions(-) diff --git a/frontend/components/admin/CompanySwitcher.tsx b/frontend/components/admin/CompanySwitcher.tsx index c37d82a5..23445780 100644 --- a/frontend/components/admin/CompanySwitcher.tsx +++ b/frontend/components/admin/CompanySwitcher.tsx @@ -174,6 +174,8 @@ export function CompanySwitcher({ onClose, isOpen = false }: CompanySwitcherProp ? "bg-accent/50 font-semibold" : "" }`} + role="button" + aria-label={`${company.company_name} ${company.company_code}`} onClick={() => handleCompanySwitch(company.company_code)} >
diff --git a/frontend/components/screen/RealtimePreviewDynamic.tsx b/frontend/components/screen/RealtimePreviewDynamic.tsx index 6330b8d4..d23337b5 100644 --- a/frontend/components/screen/RealtimePreviewDynamic.tsx +++ b/frontend/components/screen/RealtimePreviewDynamic.tsx @@ -359,10 +359,20 @@ const RealtimePreviewDynamicComponent: React.FC = ({ return `${actualHeight}px`; } - // 런타임 모드: ResponsiveGridRenderer가 ratio 기반으로 래퍼 높이를 설정하므로, - // 안쪽 컴포넌트는 "100%"로 래퍼를 채워야 비율이 정확하게 맞음 + // 런타임 모드에서 컴포넌트 타입별 높이 처리 if (!isDesignMode) { const compType = (component as any).componentType || component.componentConfig?.type || ""; + // 테이블: 부모 flex 컨테이너가 높이 관리 (flex: 1) + const flexGrowTypes = [ + "table-list", "v2-table-list", + "split-panel-layout", "split-panel-layout2", + "v2-split-panel-layout", "screen-split-panel", + "v2-tab-container", "tab-container", + "tabs-widget", "v2-tabs-widget", + ]; + if (flexGrowTypes.some(t => compType === t)) { + return "100%"; + } const autoHeightTypes = [ "table-search-widget", "v2-table-search-widget", "flow-widget", @@ -370,11 +380,9 @@ const RealtimePreviewDynamicComponent: React.FC = ({ if (autoHeightTypes.some(t => compType === t || compType.includes(t))) { return "auto"; } - // 나머지 모든 타입: 래퍼의 비율 스케일링을 따르도록 100% - return "100%"; } - // 디자인 모드: 고정 픽셀 사용 (캔버스 내 절대 좌표 배치) + // 1순위: size.height가 있으면 우선 사용 if (size?.height && size.height > 0) { if (component.componentConfig?.type === "table-list") { return `${Math.max(size.height, 200)}px`; @@ -382,14 +390,17 @@ const RealtimePreviewDynamicComponent: React.FC = ({ return `${size.height}px`; } + // 2순위: componentStyle.height (컴포넌트 정의에서 온 기본 스타일) if (componentStyle?.height) { return typeof componentStyle.height === "number" ? `${componentStyle.height}px` : componentStyle.height; } + // 3순위: 기본값 if (component.componentConfig?.type === "table-list") { return "200px"; } + // 기본 높이 return "10px"; }; @@ -592,10 +603,10 @@ const RealtimePreviewDynamicComponent: React.FC = ({ isResizing ? "none" : isOnSplitPanel ? (isDraggingSplitPanel ? "none" : "left 0.15s ease-out, width 0.15s ease-out") : undefined, } : { - // 런타임 모드: CSS scale 기반 - 캔버스 픽셀 크기 그대로 사용, 부모가 scale()로 축소 + // 런타임 모드: 부모(ResponsiveGridRenderer)가 위치/너비 관리 ...safeComponentStyle, width: "100%", - height: "100%", + height: displayHeight, position: "relative" as const, }; diff --git a/frontend/components/screen/ResponsiveGridRenderer.tsx b/frontend/components/screen/ResponsiveGridRenderer.tsx index d90a0667..1322ee99 100644 --- a/frontend/components/screen/ResponsiveGridRenderer.tsx +++ b/frontend/components/screen/ResponsiveGridRenderer.tsx @@ -23,9 +23,8 @@ function getComponentTypeId(component: ComponentData): string { } /** - * CSS transform scale 기반 렌더링. - * 디자이너와 동일하게 캔버스 해상도(px)로 레이아웃 후 CSS scale로 축소/확대. - * 텍스트, 패딩, 버튼 등 모든 요소가 균일하게 스케일링되어 WYSIWYG 보장. + * 디자이너 절대좌표를 캔버스 대비 비율로 변환하여 렌더링. + * 화면이 줄어들면 비율에 맞게 축소, 늘어나면 확대. */ function ProportionalRenderer({ components, @@ -48,7 +47,7 @@ function ProportionalRenderer({ }, []); const topLevel = components.filter((c) => !c.parentId); - const scale = containerW > 0 ? containerW / canvasWidth : 1; + const ratio = containerW > 0 ? containerW / canvasWidth : 1; const maxBottom = topLevel.reduce((max, c) => { const bottom = c.position.y + (c.size?.height || 40); @@ -59,41 +58,30 @@ function ProportionalRenderer({
0 ? `${maxBottom * scale}px` : "200px" }} + className="bg-background relative w-full overflow-x-hidden" + style={{ minHeight: containerW > 0 ? `${maxBottom * ratio}px` : "200px" }} > - {containerW > 0 && ( -
- {topLevel.map((component) => { - const typeId = getComponentTypeId(component); - return ( -
- {renderComponent(component)} -
- ); - })} -
- )} + {containerW > 0 && + topLevel.map((component) => { + const typeId = getComponentTypeId(component); + return ( +
+ {renderComponent(component)} +
+ ); + })}
); } diff --git a/frontend/lib/registry/components/v2-button-primary/ButtonPrimaryComponent.tsx b/frontend/lib/registry/components/v2-button-primary/ButtonPrimaryComponent.tsx index dc3dccc0..26a5d7c4 100644 --- a/frontend/lib/registry/components/v2-button-primary/ButtonPrimaryComponent.tsx +++ b/frontend/lib/registry/components/v2-button-primary/ButtonPrimaryComponent.tsx @@ -1410,7 +1410,7 @@ export const ButtonPrimaryComponent: React.FC = ({ const buttonElementStyle: React.CSSProperties = { width: buttonWidth, height: buttonHeight, - minHeight: undefined, // 비율 스케일링 시 래퍼 높이를 정확히 따르도록 제거 + minHeight: "32px", // 🔧 최소 높이를 32px로 줄임 // 커스텀 테두리 스타일 (StyleEditor 설정 우선, shorthand 사용 안 함) borderWidth: style?.borderWidth || "0", borderStyle: (style?.borderStyle as React.CSSProperties["borderStyle"]) || (style?.borderWidth ? "solid" : "none"), 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 fc244a3d..19a167ae 100644 --- a/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutComponent.tsx +++ b/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutComponent.tsx @@ -309,8 +309,11 @@ export const SplitPanelLayoutComponent: React.FC const [leftFilters, setLeftFilters] = useState([]); const [leftGrouping, setLeftGrouping] = useState([]); const [leftColumnVisibility, setLeftColumnVisibility] = useState([]); - const [leftColumnOrder, setLeftColumnOrder] = useState([]); // 🔧 컬럼 순서 - const [leftGroupSumConfig, setLeftGroupSumConfig] = useState(null); // 🆕 그룹별 합산 설정 + const [leftColumnOrder, setLeftColumnOrder] = useState([]); + const [leftGroupSumConfig, setLeftGroupSumConfig] = useState(null); + // 좌측 패널 컬럼 헤더 드래그 + const [leftDraggedColumnIndex, setLeftDraggedColumnIndex] = useState(null); + const [leftDropTargetColumnIndex, setLeftDropTargetColumnIndex] = useState(null); const [rightFilters, setRightFilters] = useState([]); const [rightGrouping, setRightGrouping] = useState([]); const [rightColumnVisibility, setRightColumnVisibility] = useState([]); @@ -2520,6 +2523,47 @@ export const SplitPanelLayoutComponent: React.FC } }, [selectedLeftItem, customLeftSelectedData, componentConfig, companyCode, toast, loadLeftData]); + // 좌측 패널 컬럼 헤더 드래그 + const handleLeftColumnDragStart = useCallback( + (e: React.DragEvent, columnIndex: number) => { + setLeftDraggedColumnIndex(columnIndex); + e.dataTransfer.effectAllowed = "move"; + e.dataTransfer.setData("text/plain", `left-col-${columnIndex}`); + }, + [], + ); + const handleLeftColumnDragOver = useCallback((e: React.DragEvent, columnIndex: number) => { + e.preventDefault(); + e.dataTransfer.dropEffect = "move"; + setLeftDropTargetColumnIndex(columnIndex); + }, []); + const handleLeftColumnDragEnd = useCallback(() => { + setLeftDraggedColumnIndex(null); + setLeftDropTargetColumnIndex(null); + }, []); + const handleLeftColumnDrop = useCallback( + (e: React.DragEvent, targetIndex: number) => { + e.preventDefault(); + const fromIdx = leftDraggedColumnIndex; + if (fromIdx === null || fromIdx === targetIndex) { + handleLeftColumnDragEnd(); + return; + } + const leftColumns = componentConfig.leftPanel?.columns || []; + const colNames = leftColumns + .filter((c: any) => typeof c === "string" || c.name || c.columnName) + .map((c: any) => typeof c === "string" ? c : c.name || c.columnName); + if (colNames.length > 0 && fromIdx >= 0 && fromIdx < colNames.length && targetIndex >= 0 && targetIndex < colNames.length) { + const reordered = [...colNames]; + const [removed] = reordered.splice(fromIdx, 1); + reordered.splice(targetIndex, 0, removed); + setLeftColumnOrder(reordered); + } + handleLeftColumnDragEnd(); + }, + [leftDraggedColumnIndex, componentConfig, handleLeftColumnDragEnd], + ); + // 우측 패널 컬럼 헤더 드래그 (디자인 모드에서 컬럼 순서 변경) const handleRightColumnDragStart = useCallback( (e: React.DragEvent, columnIndex: number, source: "main" | number) => { @@ -3568,6 +3612,7 @@ export const SplitPanelLayoutComponent: React.FC (componentConfig.leftPanel?.showEdit !== false) || (componentConfig.leftPanel?.showDelete !== false) ); + const canDragLeftGroupedColumns = !isDesignMode && columnsToShow.length > 1; if (groupedLeftData.length > 0) { return (
@@ -3579,18 +3624,33 @@ export const SplitPanelLayoutComponent: React.FC
onDragEnd={handleRightColumnDragEnd} onDrop={(e) => canDragListTabColumns && handleRightColumnDrop(e, idx, listTabIndex)} > + {canDragListTabColumns && } {col.label || col.name}
onDragEnd={handleRightColumnDragEnd} onDrop={(e) => isDraggable && handleRightColumnDrop(e, configColIndex, "main")} > + {isDraggable && } {col.label}
100 ? `${leftTotalColWidth}%` : '100%' }}> - {columnsToShow.map((col, idx) => ( + {columnsToShow.map((col, idx) => { + const isDropTarget = canDragLeftGroupedColumns && leftDropTargetColumnIndex === idx; + const isDragging = canDragLeftGroupedColumns && leftDraggedColumnIndex === idx; + return ( - ))} + ); + })} {hasGroupedLeftActions && ( @@ -3671,23 +3731,39 @@ export const SplitPanelLayoutComponent: React.FC (componentConfig.leftPanel?.showEdit !== false) || (componentConfig.leftPanel?.showDelete !== false) ); + const canDragLeftColumns = !isDesignMode && columnsToShow.length > 1; return (
canDragLeftGroupedColumns && handleLeftColumnDragStart(e, idx)} + onDragOver={(e) => canDragLeftGroupedColumns && handleLeftColumnDragOver(e, idx)} + onDragEnd={handleLeftColumnDragEnd} + onDrop={(e) => canDragLeftGroupedColumns && handleLeftColumnDrop(e, idx)} > + {canDragLeftGroupedColumns && } {col.label}
100 ? `${leftTotalColWidth}%` : '100%' }}> - {columnsToShow.map((col, idx) => ( + {columnsToShow.map((col, idx) => { + const isDropTarget = canDragLeftColumns && leftDropTargetColumnIndex === idx; + const isDragging = canDragLeftColumns && leftDraggedColumnIndex === idx; + return ( - ))} + ); + })} {hasLeftTableActions && ( @@ -4161,9 +4237,11 @@ export const SplitPanelLayoutComponent: React.FC // 테이블 모드로 표시 (행 클릭 시 상세 정보 펼치기) if (currentTabConfig?.displayMode === "table") { const hasTabActions = currentTabConfig?.showEdit || currentTabConfig?.showDelete; - // showInSummary가 false가 아닌 것만 메인 테이블에 표시 - const tabSummaryColumns = tabColumns.filter((col: any) => col.showInSummary !== false); const tabIndex = activeTabIndex - 1; + let tabSummaryColumns = tabColumns.filter((col: any) => col.showInSummary !== false); + if (!isDesignMode) { + tabSummaryColumns = applyRuntimeOrder(tabSummaryColumns, tabIndex); + } const canDragTabColumns = tabSummaryColumns.length > 0; return (
@@ -4188,7 +4266,7 @@ export const SplitPanelLayoutComponent: React.FC onDragEnd={handleRightColumnDragEnd} onDrop={(e) => canDragTabColumns && handleRightColumnDrop(e, idx, tabIndex)} > - {canDragTabColumns && } + {canDragTabColumns && } {col.label || col.name} ); @@ -4298,9 +4376,11 @@ 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; + let listSummaryColumns = tabColumns.filter((col: any) => col.showInSummary !== false); + if (!isDesignMode) { + listSummaryColumns = applyRuntimeOrder(listSummaryColumns, listTabIndex); + } const canDragListTabColumns = listSummaryColumns.length > 0; return (
@@ -4325,7 +4405,7 @@ export const SplitPanelLayoutComponent: React.FC onDragEnd={handleRightColumnDragEnd} onDrop={(e) => canDragListTabColumns && handleRightColumnDrop(e, idx, listTabIndex)} > - {canDragListTabColumns && } + {canDragListTabColumns && } {col.label || col.name} ); @@ -4752,7 +4832,7 @@ export const SplitPanelLayoutComponent: React.FC onDragEnd={handleRightColumnDragEnd} onDrop={(e) => isDraggable && handleRightColumnDrop(e, configColIndex, "main")} > - {isDraggable && } + {isDraggable && } {col.label} ); From b2a569f908993593a3efeb4bebdf9e033329ce0a Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Wed, 18 Mar 2026 00:05:40 +0900 Subject: [PATCH 22/23] 123 --- .../screen/RealtimePreviewDynamic.tsx | 6 +-- .../screen/ResponsiveGridRenderer.tsx | 18 +++----- .../v2-table-list/SingleTableWithSticky.tsx | 35 ++++++++-------- .../v2-table-list/TableListComponent.tsx | 41 +++++++++++-------- 4 files changed, 51 insertions(+), 49 deletions(-) diff --git a/frontend/components/screen/RealtimePreviewDynamic.tsx b/frontend/components/screen/RealtimePreviewDynamic.tsx index d23337b5..ce76857b 100644 --- a/frontend/components/screen/RealtimePreviewDynamic.tsx +++ b/frontend/components/screen/RealtimePreviewDynamic.tsx @@ -362,15 +362,15 @@ const RealtimePreviewDynamicComponent: React.FC = ({ // 런타임 모드에서 컴포넌트 타입별 높이 처리 if (!isDesignMode) { const compType = (component as any).componentType || component.componentConfig?.type || ""; - // 테이블: 부모 flex 컨테이너가 높이 관리 (flex: 1) - const flexGrowTypes = [ + // 레이아웃 계열: 부모 래퍼를 꽉 채움 (ResponsiveGridRenderer가 % 높이 관리) + const fillParentTypes = [ "table-list", "v2-table-list", "split-panel-layout", "split-panel-layout2", "v2-split-panel-layout", "screen-split-panel", "v2-tab-container", "tab-container", "tabs-widget", "v2-tabs-widget", ]; - if (flexGrowTypes.some(t => compType === t)) { + if (fillParentTypes.some(t => compType === t)) { return "100%"; } const autoHeightTypes = [ diff --git a/frontend/components/screen/ResponsiveGridRenderer.tsx b/frontend/components/screen/ResponsiveGridRenderer.tsx index 1322ee99..47a2cd52 100644 --- a/frontend/components/screen/ResponsiveGridRenderer.tsx +++ b/frontend/components/screen/ResponsiveGridRenderer.tsx @@ -23,8 +23,9 @@ function getComponentTypeId(component: ComponentData): string { } /** - * 디자이너 절대좌표를 캔버스 대비 비율로 변환하여 렌더링. - * 화면이 줄어들면 비율에 맞게 축소, 늘어나면 확대. + * 디자이너 절대좌표를 캔버스 대비 비율(%)로 변환하여 렌더링. + * 가로: 컨테이너 너비 대비 % → 반응형 스케일 + * 세로: 컨테이너 높이 대비 % → 뷰포트에 맞게 자동 조절 */ function ProportionalRenderer({ components, @@ -47,19 +48,12 @@ function ProportionalRenderer({ }, []); const topLevel = components.filter((c) => !c.parentId); - const ratio = containerW > 0 ? containerW / canvasWidth : 1; - - const maxBottom = topLevel.reduce((max, c) => { - const bottom = c.position.y + (c.size?.height || 40); - return Math.max(max, bottom); - }, 0); return (
0 ? `${maxBottom * ratio}px` : "200px" }} + className="bg-background relative h-full w-full overflow-hidden" > {containerW > 0 && topLevel.map((component) => { @@ -72,9 +66,9 @@ function ProportionalRenderer({ style={{ position: "absolute", left: `${(component.position.x / canvasWidth) * 100}%`, - top: `${component.position.y * ratio}px`, + top: `${(component.position.y / canvasHeight) * 100}%`, width: `${((component.size?.width || 100) / canvasWidth) * 100}%`, - height: `${(component.size?.height || 40) * ratio}px`, + height: `${((component.size?.height || 40) / canvasHeight) * 100}%`, zIndex: component.position.z || 1, }} > diff --git a/frontend/lib/registry/components/v2-table-list/SingleTableWithSticky.tsx b/frontend/lib/registry/components/v2-table-list/SingleTableWithSticky.tsx index c264dec2..3a7b4dad 100644 --- a/frontend/lib/registry/components/v2-table-list/SingleTableWithSticky.tsx +++ b/frontend/lib/registry/components/v2-table-list/SingleTableWithSticky.tsx @@ -144,28 +144,33 @@ export const SingleTableWithSticky: React.FC = ({ isDesignMode && column.hidden && "bg-muted/50 opacity-40", )} style={{ - width: getColumnWidth(column), - minWidth: "100px", // 최소 너비 보장 - maxWidth: "300px", // 최대 너비 제한 + width: column.columnName === "__checkbox__" ? 48 : getColumnWidth(column), + minWidth: column.columnName === "__checkbox__" ? "48px" : "100px", + maxWidth: column.columnName === "__checkbox__" ? "48px" : "300px", boxSizing: "border-box", overflow: "hidden", textOverflow: "ellipsis", - whiteSpace: "nowrap", // 텍스트 줄바꿈 방지 + whiteSpace: "nowrap", backgroundColor: "hsl(var(--muted) / 0.4)", - // sticky 위치 설정 ...(column.fixed === "left" && { left: leftFixedWidth }), ...(column.fixed === "right" && { right: rightFixedWidth }), }} onClick={() => column.sortable && sortHandler(column.columnName)} > -
+
{column.columnName === "__checkbox__" ? ( checkboxConfig.selectAll && ( ) ) : ( @@ -327,26 +332,22 @@ export const SingleTableWithSticky: React.FC = ({ key={`cell-${column.columnName}`} id={isCurrentSearchResult ? "current-search-result" : undefined} className={cn( - "text-foreground h-10 px-3 py-[7px] align-middle text-[11px] transition-colors", - // 이미지 셀은 overflow/ellipsis 제외 (이미지 잘림 방지) + "text-foreground h-10 align-middle text-[11px] transition-colors", + column.columnName === "__checkbox__" ? "px-0 py-[7px] text-center" : "px-3 py-[7px]", !isReactElement && "whitespace-nowrap", - `text-${column.align}`, - // 고정 컬럼 스타일 + column.columnName !== "__checkbox__" && `text-${column.align}`, column.fixed === "left" && "border-border bg-background/90 sticky z-10 border-r backdrop-blur-sm", column.fixed === "right" && "border-border bg-background/90 sticky z-10 border-l backdrop-blur-sm", - // 편집 가능 셀 스타일 onCellDoubleClick && column.columnName !== "__checkbox__" && "cursor-text", )} style={{ - width: getColumnWidth(column), - minWidth: "100px", // 최소 너비 보장 - maxWidth: "300px", // 최대 너비 제한 + width: column.columnName === "__checkbox__" ? 48 : getColumnWidth(column), + minWidth: column.columnName === "__checkbox__" ? "48px" : "100px", + maxWidth: column.columnName === "__checkbox__" ? "48px" : "300px", boxSizing: "border-box", - // 이미지 셀은 overflow 허용 ...(isReactElement ? {} : { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }), - // sticky 위치 설정 ...(column.fixed === "left" && { left: leftFixedWidth }), ...(column.fixed === "right" && { right: rightFixedWidth }), }} diff --git a/frontend/lib/registry/components/v2-table-list/TableListComponent.tsx b/frontend/lib/registry/components/v2-table-list/TableListComponent.tsx index 7331c7bd..3f41b185 100644 --- a/frontend/lib/registry/components/v2-table-list/TableListComponent.tsx +++ b/frontend/lib/registry/components/v2-table-list/TableListComponent.tsx @@ -878,10 +878,7 @@ export const TableListComponent: React.FC = ({ const [isTableOptionsOpen, setIsTableOptionsOpen] = useState(false); const [showGridLines, setShowGridLines] = useState(true); const [viewMode, setViewMode] = useState<"table" | "card" | "grouped-card">("table"); - // 체크박스 컬럼은 항상 기본 틀고정 - const [frozenColumns, setFrozenColumns] = useState( - (tableConfig.checkbox?.enabled ?? true) ? ["__checkbox__"] : [], - ); + const [frozenColumns, setFrozenColumns] = useState([]); const [frozenColumnCount, setFrozenColumnCount] = useState(0); // 🆕 Search Panel (통합 검색) 관련 상태 @@ -1173,14 +1170,10 @@ export const TableListComponent: React.FC = ({ frozenColumnCount, // 현재 틀고정 컬럼 수 onFrozenColumnCountChange: (count: number) => { setFrozenColumnCount(count); - // 체크박스 컬럼은 항상 틀고정에 포함 - const checkboxColumn = (tableConfig.checkbox?.enabled ?? true) ? ["__checkbox__"] : []; - // 표시 가능한 컬럼 중 처음 N개를 틀고정 컬럼으로 설정 const visibleCols = columnsToRegister .filter((col) => col.visible !== false) .map((col) => col.columnName || col.field); - const newFrozenColumns = [...checkboxColumn, ...visibleCols.slice(0, count)]; - setFrozenColumns(newFrozenColumns); + setFrozenColumns(visibleCols.slice(0, count)); }, // 탭 관련 정보 (탭 내부의 테이블인 경우) parentTabId, @@ -3080,11 +3073,7 @@ export const TableListComponent: React.FC = ({ if (state.groupByColumns) setGroupByColumns(state.groupByColumns); if (state.frozenColumns) { // 체크박스 컬럼이 항상 포함되도록 보장 - const checkboxColumn = (tableConfig.checkbox?.enabled ?? true) ? "__checkbox__" : null; - const restoredFrozenColumns = - checkboxColumn && !state.frozenColumns.includes(checkboxColumn) - ? [checkboxColumn, ...state.frozenColumns] - : state.frozenColumns; + const restoredFrozenColumns = (state.frozenColumns || []).filter((col: string) => col !== "__checkbox__"); setFrozenColumns(restoredFrozenColumns); } if (state.frozenColumnCount !== undefined) setFrozenColumnCount(state.frozenColumnCount); // 틀고정 컬럼 수 복원 @@ -4233,7 +4222,19 @@ export const TableListComponent: React.FC = ({ if (!tableConfig.checkbox?.selectAll) return null; if (tableConfig.checkbox?.multiple === false) return null; - return ; + return ( + + ); }; const renderCheckboxCell = (row: any, index: number) => { @@ -4245,6 +4246,12 @@ export const TableListComponent: React.FC = ({ checked={isChecked} onCheckedChange={(checked) => handleRowSelection(rowKey, checked as boolean)} aria-label={`행 ${index + 1} 선택`} + style={{ + width: 16, + height: 16, + borderWidth: 1.5, + borderColor: isChecked ? "hsl(var(--primary))" : "hsl(var(--muted-foreground) / 0.5)", + }} /> ); }; @@ -6201,7 +6208,7 @@ export const TableListComponent: React.FC = ({ "text-foreground text-[11px] font-normal", inputType !== "image" && "overflow-hidden text-ellipsis whitespace-nowrap max-w-[170px]", column.columnName === "__checkbox__" - ? "px-0 py-[7px]" + ? "px-0 py-[7px] text-center" : "px-3 py-[7px]", isFrozen && "sticky z-20 shadow-[2px_0_4px_rgba(0,0,0,0.08)]", (inputType === "code" || inputType === "category") && "font-mono text-[10px] text-primary font-medium", @@ -6370,7 +6377,7 @@ export const TableListComponent: React.FC = ({ className={cn( "text-foreground text-[11px] font-normal", inputType !== "image" && "overflow-hidden text-ellipsis whitespace-nowrap max-w-[170px]", - column.columnName === "__checkbox__" ? "px-0 py-[7px]" : "px-3 py-[7px]", + column.columnName === "__checkbox__" ? "px-0 py-[7px] text-center" : "px-3 py-[7px]", isFrozen && "sticky z-20 shadow-[2px_0_4px_rgba(0,0,0,0.08)]", isCellFocused && !editingCell && "ring-primary bg-primary/5 ring-2 ring-inset", editingCell?.rowIndex === index && editingCell?.colIndex === colIndex && "p-0", From 8630d82a69b021c0b97ad7f2b3c02f98b450eecd Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Wed, 18 Mar 2026 10:16:59 +0900 Subject: [PATCH 23/23] Refactor RealtimePreviewDynamic and ResponsiveGridRenderer components to improve layout handling and responsiveness. Update SingleTableWithSticky and TableListComponent for better checkbox handling and styling consistency. Enhance overall user experience with refined component structures and styles. --- .../v2-table-list/TableListComponent.tsx | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/frontend/lib/registry/components/v2-table-list/TableListComponent.tsx b/frontend/lib/registry/components/v2-table-list/TableListComponent.tsx index 3f41b185..31cabb70 100644 --- a/frontend/lib/registry/components/v2-table-list/TableListComponent.tsx +++ b/frontend/lib/registry/components/v2-table-list/TableListComponent.tsx @@ -3072,7 +3072,7 @@ export const TableListComponent: React.FC = ({ if (state.sortDirection) setSortDirection(state.sortDirection); if (state.groupByColumns) setGroupByColumns(state.groupByColumns); if (state.frozenColumns) { - // 체크박스 컬럼이 항상 포함되도록 보장 + // 체크박스 컬럼은 frozen 대상에서 제외 (배경색 이중 적용 방지) const restoredFrozenColumns = (state.frozenColumns || []).filter((col: string) => col !== "__checkbox__"); setFrozenColumns(restoredFrozenColumns); } @@ -6227,16 +6227,18 @@ export const TableListComponent: React.FC = ({ : `${100 / visibleColumns.length}%`, minWidth: column.columnName === "__checkbox__" ? "48px" : undefined, maxWidth: column.columnName === "__checkbox__" ? "48px" : undefined, - ...(isFrozen && { - left: `${leftPosition}px`, - backgroundColor: "hsl(var(--background))", - }), - }} - > + ...(isFrozen && { + left: `${leftPosition}px`, + backgroundColor: index % 2 === 0 + ? "hsl(var(--background))" + : "hsl(var(--muted) / 0.2)", + }), + }} + > {column.columnName === "__checkbox__" ? renderCheckboxCell(row, index) : formatCellValue(cellValue, column, row)} - + ); })}
@@ -6384,7 +6386,7 @@ export const TableListComponent: React.FC = ({ isModified && !cellValidationError && "bg-amber-100 dark:bg-amber-900/40", cellValidationError && "bg-red-50 ring-2 ring-red-500 ring-inset dark:bg-red-950/40", isSearchHighlighted && !isCellFocused && "bg-yellow-200 dark:bg-yellow-700/50", - column.editable === false && "bg-gray-50 dark:bg-gray-900/30", + column.editable === false && "bg-muted/10 dark:bg-muted/10", // 코드 컬럼: mono 폰트 + primary 색상 (inputType === "code" || inputType === "category") && "font-mono text-[10px] text-primary font-medium", // 숫자 컬럼: tabular-nums 오른쪽 정렬 @@ -6405,7 +6407,9 @@ export const TableListComponent: React.FC = ({ maxWidth: column.columnName === "__checkbox__" ? "48px" : undefined, ...(isFrozen && { left: `${leftPosition}px`, - backgroundColor: "hsl(var(--background))", + backgroundColor: index % 2 === 0 + ? "hsl(var(--background))" + : "hsl(var(--muted) / 0.2)", }), }} onClick={(e) => handleCellClick(index, colIndex, e)}
canDragLeftColumns && handleLeftColumnDragStart(e, idx)} + onDragOver={(e) => canDragLeftColumns && handleLeftColumnDragOver(e, idx)} + onDragEnd={handleLeftColumnDragEnd} + onDrop={(e) => canDragLeftColumns && handleLeftColumnDrop(e, idx)} > + {canDragLeftColumns && } {col.label}