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) => ( ))} {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 ? (
)} - {(componentConfig.rightPanel?.deleteButton?.enabled ?? true) && ( + {rightDeleteVisible && (
- {/* 🆕 배치 편집 툴바 */} + {/* 필터 칩 바 */} + {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 = ({ )} {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" ); };