[agent-pipeline] pipe-20260317084014-ydap round-1
This commit is contained in:
parent
9409f1308f
commit
d3acf391a4
|
|
@ -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() {
|
|||
<p className="text-sm font-medium">변경될 PK 컬럼:</p>
|
||||
{pendingPkColumns.length > 0 ? (
|
||||
<div className="mt-2 flex flex-wrap gap-2">
|
||||
{pendingPkColumns.map((col) => (
|
||||
<Badge key={col} variant="secondary" className="font-mono text-xs">
|
||||
{col}
|
||||
{pendingPkColumns.map((col) => {
|
||||
const colInfo = columns.find((c) => c.columnName === col);
|
||||
return (
|
||||
<Badge key={col} variant="secondary" className="text-xs">
|
||||
{colInfo?.displayName && colInfo.displayName !== col
|
||||
? `${colInfo.displayName} (${col})`
|
||||
: col}
|
||||
</Badge>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-destructive mt-2 text-sm">PK가 모두 제거됩니다</p>
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className="flex h-full w-full flex-col border-l bg-card">
|
||||
|
|
@ -90,7 +99,11 @@ export function ColumnDetailPanel({
|
|||
{typeConf.label}
|
||||
</span>
|
||||
)}
|
||||
<span className="truncate font-mono text-sm font-medium">{column.columnName}</span>
|
||||
<span className="truncate text-sm font-medium">
|
||||
{column.displayName && column.displayName !== column.columnName
|
||||
? `${column.displayName} (${column.columnName})`
|
||||
: column.columnName}
|
||||
</span>
|
||||
</div>
|
||||
<Button type="button" variant="ghost" size="icon" className="h-8 w-8 shrink-0" onClick={onClose} aria-label="닫기">
|
||||
<X className="h-4 w-4" />
|
||||
|
|
@ -207,7 +220,12 @@ export function ColumnDetailPanel({
|
|||
className="h-9 w-full justify-between text-xs"
|
||||
>
|
||||
{column.referenceColumn && column.referenceColumn !== "none"
|
||||
? column.referenceColumn
|
||||
? (() => {
|
||||
const matched = refColumns.find((c) => c.columnName === column.referenceColumn);
|
||||
return matched?.displayName && matched.displayName !== column.referenceColumn
|
||||
? `${matched.displayName} (${column.referenceColumn})`
|
||||
: column.referenceColumn;
|
||||
})()
|
||||
: "컬럼 선택..."}
|
||||
<ChevronsUpDown className="ml-2 h-3 w-3 opacity-50" />
|
||||
</Button>
|
||||
|
|
@ -245,7 +263,13 @@ export function ColumnDetailPanel({
|
|||
column.referenceColumn === refCol.columnName ? "opacity-100" : "opacity-0",
|
||||
)}
|
||||
/>
|
||||
{refCol.columnName}
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">
|
||||
{refCol.displayName && refCol.displayName !== refCol.columnName
|
||||
? `${refCol.displayName} (${refCol.columnName})`
|
||||
: refCol.columnName}
|
||||
</span>
|
||||
</div>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
|
|
@ -259,12 +283,20 @@ export function ColumnDetailPanel({
|
|||
{/* 참조 요약 미니맵 */}
|
||||
{column.referenceTable && column.referenceTable !== "none" && column.referenceColumn && (
|
||||
<div className="flex items-center gap-2 rounded-md bg-violet-50 px-3 py-2">
|
||||
<span className="font-mono text-[11px] font-semibold text-violet-600">
|
||||
{column.referenceTable}
|
||||
<span className="text-[11px] font-semibold text-violet-600">
|
||||
{(() => {
|
||||
const tbl = refTableOpts.find((o) => o.value === column.referenceTable);
|
||||
return tbl?.label ?? column.referenceTable;
|
||||
})()}
|
||||
</span>
|
||||
<span className="text-muted-foreground text-[10px]">→</span>
|
||||
<span className="font-mono text-[11px] font-semibold text-violet-600">
|
||||
{column.referenceColumn}
|
||||
<span className="text-[11px] font-semibold text-violet-600">
|
||||
{(() => {
|
||||
const col = refColumns.find((c) => c.columnName === column.referenceColumn);
|
||||
return col?.displayName && col.displayName !== column.referenceColumn
|
||||
? `${col.displayName} (${column.referenceColumn})`
|
||||
: column.referenceColumn;
|
||||
})()}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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<string, ReferenceTableColumn[]>;
|
||||
}
|
||||
|
||||
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 색상바 (타입별 진한 색) */}
|
||||
<div className={cn("h-full min-h-8 w-1 rounded-full", typeConf.barColor)} />
|
||||
|
||||
{/* 라벨 + 컬럼명 */}
|
||||
{/* 라벨 + 컬럼명 (한글라벨 (영어명) 동시 표시) */}
|
||||
<div className="min-w-0">
|
||||
<div className="truncate text-sm font-medium">
|
||||
{column.displayName || column.columnName}
|
||||
</div>
|
||||
<div className="truncate font-mono text-xs text-muted-foreground">
|
||||
{column.columnName}
|
||||
{column.displayName && column.displayName !== column.columnName
|
||||
? `${column.displayName} (${column.columnName})`
|
||||
: column.columnName}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -150,11 +155,38 @@ export function ColumnGrid({
|
|||
<div className="flex min-w-0 flex-wrap gap-1">
|
||||
{column.inputType === "entity" && column.referenceTable && column.referenceTable !== "none" && (
|
||||
<>
|
||||
<Badge variant="outline" className="text-xs font-normal">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-xs font-normal"
|
||||
title={
|
||||
tables
|
||||
? (() => {
|
||||
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}
|
||||
</Badge>
|
||||
<span className="text-muted-foreground text-xs">→</span>
|
||||
<Badge variant="outline" className="text-xs font-normal">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-xs font-normal"
|
||||
title={
|
||||
referenceTableColumns?.[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 || "—"}
|
||||
</Badge>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className="bg-background flex h-screen flex-col">
|
||||
{/* 모바일 헤더 */}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ function TabsList({
|
|||
<TabsPrimitive.List
|
||||
data-slot="tabs-list"
|
||||
className={cn(
|
||||
"bg-muted/30 text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]",
|
||||
"bg-muted/50 text-muted-foreground inline-flex h-10 w-fit items-center justify-center rounded-lg border border-border/50 p-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
|
@ -42,7 +42,7 @@ function TabsTrigger({
|
|||
<TabsPrimitive.Trigger
|
||||
data-slot="tabs-trigger"
|
||||
className={cn(
|
||||
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
"data-[state=active]:bg-background data-[state=active]:font-semibold dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-foreground/70 inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
|
|
|||
|
|
@ -3981,8 +3981,8 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
|||
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<SplitPanelLayoutComponentProps>
|
|||
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}`}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className="flex h-full w-full flex-col overflow-hidden rounded-lg border bg-background">
|
||||
{/* 탭 헤더 */}
|
||||
<div className="flex items-center border-b bg-muted/30">
|
||||
<div className="flex items-center border-b bg-muted/50">
|
||||
{tabs.length > 0 ? (
|
||||
tabs.map((tab) => (
|
||||
<div
|
||||
|
|
@ -649,8 +649,8 @@ ComponentRegistry.registerComponent({
|
|||
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"
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -662,7 +662,7 @@ ComponentRegistry.registerComponent({
|
|||
onDragEnd={onDragEnd}
|
||||
>
|
||||
{/* 탭 헤더 */}
|
||||
<div className="flex items-center border-b bg-muted/30">
|
||||
<div className="flex items-center border-b bg-muted/50">
|
||||
{tabs.length > 0 ? (
|
||||
tabs.map((tab) => (
|
||||
<div
|
||||
|
|
|
|||
Loading…
Reference in New Issue