From d3acf391a40962aeb92c21cbf778d6ef1773ffdb Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Tue, 17 Mar 2026 18:05:10 +0900 Subject: [PATCH] [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) => (