[agent-pipeline] pipe-20260317084014-ydap round-1

This commit is contained in:
DDD1542 2026-03-17 18:05:10 +09:00
parent 9409f1308f
commit d3acf391a4
8 changed files with 164 additions and 40 deletions

View File

@ -217,10 +217,16 @@ export default function TableManagementPage() {
// 메모이제이션된 입력타입 옵션 // 메모이제이션된 입력타입 옵션
const memoizedInputTypeOptions = useMemo(() => inputTypeOptions, []); const memoizedInputTypeOptions = useMemo(() => inputTypeOptions, []);
// 참조 테이블 옵션 (실제 테이블 목록에서 가져옴) // 참조 테이블 옵션 (한글라벨 (영어명) 동시 표시)
const referenceTableOptions = [ const referenceTableOptions = [
{ value: "none", label: getTextFromUI(TABLE_MANAGEMENT_KEYS.LABEL_NONE, "선택 안함") }, { 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) => onIndexToggle={(columnName, checked) =>
handleIndexToggle(columnName, "index", 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> <p className="text-sm font-medium"> PK :</p>
{pendingPkColumns.length > 0 ? ( {pendingPkColumns.length > 0 ? (
<div className="mt-2 flex flex-wrap gap-2"> <div className="mt-2 flex flex-wrap gap-2">
{pendingPkColumns.map((col) => ( {pendingPkColumns.map((col) => {
<Badge key={col} variant="secondary" className="font-mono text-xs"> const colInfo = columns.find((c) => c.columnName === col);
{col} return (
<Badge key={col} variant="secondary" className="text-xs">
{colInfo?.displayName && colInfo.displayName !== col
? `${colInfo.displayName} (${col})`
: col}
</Badge> </Badge>
))} );
})}
</div> </div>
) : ( ) : (
<p className="text-destructive mt-2 text-sm">PK가 </p> <p className="text-destructive mt-2 text-sm">PK가 </p>

View File

@ -78,7 +78,16 @@ export function ColumnDetailPanel({
const refTableOpts = referenceTableOptions.length const refTableOpts = referenceTableOptions.length
? referenceTableOptions ? 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 ( return (
<div className="flex h-full w-full flex-col border-l bg-card"> <div className="flex h-full w-full flex-col border-l bg-card">
@ -90,7 +99,11 @@ export function ColumnDetailPanel({
{typeConf.label} {typeConf.label}
</span> </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> </div>
<Button type="button" variant="ghost" size="icon" className="h-8 w-8 shrink-0" onClick={onClose} aria-label="닫기"> <Button type="button" variant="ghost" size="icon" className="h-8 w-8 shrink-0" onClick={onClose} aria-label="닫기">
<X className="h-4 w-4" /> <X className="h-4 w-4" />
@ -207,7 +220,12 @@ export function ColumnDetailPanel({
className="h-9 w-full justify-between text-xs" className="h-9 w-full justify-between text-xs"
> >
{column.referenceColumn && column.referenceColumn !== "none" {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" /> <ChevronsUpDown className="ml-2 h-3 w-3 opacity-50" />
</Button> </Button>
@ -245,7 +263,13 @@ export function ColumnDetailPanel({
column.referenceColumn === refCol.columnName ? "opacity-100" : "opacity-0", 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> </CommandItem>
))} ))}
</CommandGroup> </CommandGroup>
@ -259,12 +283,20 @@ export function ColumnDetailPanel({
{/* 참조 요약 미니맵 */} {/* 참조 요약 미니맵 */}
{column.referenceTable && column.referenceTable !== "none" && column.referenceColumn && ( {column.referenceTable && column.referenceTable !== "none" && column.referenceColumn && (
<div className="flex items-center gap-2 rounded-md bg-violet-50 px-3 py-2"> <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"> <span className="text-[11px] font-semibold text-violet-600">
{column.referenceTable} {(() => {
const tbl = refTableOpts.find((o) => o.value === column.referenceTable);
return tbl?.label ?? column.referenceTable;
})()}
</span> </span>
<span className="text-muted-foreground text-[10px]"></span> <span className="text-muted-foreground text-[10px]"></span>
<span className="font-mono text-[11px] font-semibold text-violet-600"> <span className="text-[11px] font-semibold text-violet-600">
{column.referenceColumn} {(() => {
const col = refColumns.find((c) => c.columnName === column.referenceColumn);
return col?.displayName && col.displayName !== column.referenceColumn
? `${col.displayName} (${column.referenceColumn})`
: column.referenceColumn;
})()}
</span> </span>
</div> </div>
)} )}

View File

@ -5,8 +5,9 @@ import { MoreHorizontal, Database, Layers, FileStack } from "lucide-react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import type { ColumnTypeInfo } from "./types"; import type { ColumnTypeInfo, TableInfo } from "./types";
import { INPUT_TYPE_COLORS, getColumnGroup } from "./types"; import { INPUT_TYPE_COLORS, getColumnGroup } from "./types";
import type { ReferenceTableColumn } from "@/lib/api/entityJoin";
export interface ColumnGridConstraints { export interface ColumnGridConstraints {
primaryKey: { columns: string[] }; primaryKey: { columns: string[] };
@ -23,6 +24,9 @@ export interface ColumnGridProps {
getColumnIndexState?: (columnName: string) => { isPk: boolean; hasIndex: boolean }; getColumnIndexState?: (columnName: string) => { isPk: boolean; hasIndex: boolean };
onPkToggle?: (columnName: string, checked: boolean) => void; onPkToggle?: (columnName: string, checked: boolean) => void;
onIndexToggle?: (columnName: string, checked: boolean) => void; onIndexToggle?: (columnName: string, checked: boolean) => void;
/** 호버 시 한글 라벨 표시용 (Badge title) */
tables?: TableInfo[];
referenceTableColumns?: Record<string, ReferenceTableColumn[]>;
} }
function getIndexState( function getIndexState(
@ -53,6 +57,8 @@ export function ColumnGrid({
getColumnIndexState: externalGetIndexState, getColumnIndexState: externalGetIndexState,
onPkToggle, onPkToggle,
onIndexToggle, onIndexToggle,
tables,
referenceTableColumns,
}: ColumnGridProps) { }: ColumnGridProps) {
const getIdxState = useMemo( const getIdxState = useMemo(
() => externalGetIndexState ?? ((name: string) => getIndexState(name, constraints)), () => externalGetIndexState ?? ((name: string) => getIndexState(name, constraints)),
@ -136,13 +142,12 @@ export function ColumnGrid({
{/* 4px 색상바 (타입별 진한 색) */} {/* 4px 색상바 (타입별 진한 색) */}
<div className={cn("h-full min-h-8 w-1 rounded-full", typeConf.barColor)} /> <div className={cn("h-full min-h-8 w-1 rounded-full", typeConf.barColor)} />
{/* 라벨 + 컬럼명 */} {/* 라벨 + 컬럼명 (한글라벨 (영어명) 동시 표시) */}
<div className="min-w-0"> <div className="min-w-0">
<div className="truncate text-sm font-medium"> <div className="truncate text-sm font-medium">
{column.displayName || column.columnName} {column.displayName && column.displayName !== column.columnName
</div> ? `${column.displayName} (${column.columnName})`
<div className="truncate font-mono text-xs text-muted-foreground"> : column.columnName}
{column.columnName}
</div> </div>
</div> </div>
@ -150,11 +155,38 @@ export function ColumnGrid({
<div className="flex min-w-0 flex-wrap gap-1"> <div className="flex min-w-0 flex-wrap gap-1">
{column.inputType === "entity" && column.referenceTable && column.referenceTable !== "none" && ( {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} {column.referenceTable}
</Badge> </Badge>
<span className="text-muted-foreground text-xs"></span> <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 || "—"} {column.referenceColumn || "—"}
</Badge> </Badge>
</> </>

View File

@ -1,6 +1,6 @@
"use client"; "use client";
import { useState, Suspense, useEffect } from "react"; import { useState, Suspense, useEffect, useCallback } from "react";
import { useRouter, usePathname, useSearchParams } from "next/navigation"; import { useRouter, usePathname, useSearchParams } from "next/navigation";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
@ -341,6 +341,10 @@ function AppLayoutInner({ children }: AppLayoutProps) {
const currentMenus = isAdminMode ? adminMenus : userMenus; 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 toggleMenu = (menuId: string) => {
const newExpanded = new Set(expandedMenus); const newExpanded = new Set(expandedMenus);
if (newExpanded.has(menuId)) { 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 스타일 적용) // 메뉴 트리 렌더링 (기존 MainLayout 스타일 적용)
const renderMenu = (menu: any, level: number = 0) => { const renderMenu = (menu: any, level: number = 0) => {
const isExpanded = expandedMenus.has(menu.id); const isExpanded = expandedMenus.has(menu.id);
@ -489,8 +513,8 @@ function AppLayoutInner({ children }: AppLayoutProps) {
draggable={isLeaf} draggable={isLeaf}
onDragStart={(e) => handleMenuDragStart(e, menu)} 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] ${ 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 isMenuActive(menu)
? "border-primary bg-primary/8 text-primary border-l-3 font-semibold" ? "border-l-[3px] border-l-primary bg-primary/10 dark:bg-primary/15 text-primary font-semibold"
: isExpanded : isExpanded
? "bg-accent/60 text-foreground" ? "bg-accent/60 text-foreground"
: "text-muted-foreground hover:bg-accent hover:text-foreground" : "text-muted-foreground hover:bg-accent hover:text-foreground"
@ -518,8 +542,8 @@ function AppLayoutInner({ children }: AppLayoutProps) {
draggable={!child.hasChildren} draggable={!child.hasChildren}
onDragStart={(e) => handleMenuDragStart(e, child)} 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] ${ 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 isMenuActive(child)
? "border-primary bg-primary/8 text-primary border-l-3 font-semibold" ? "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" : "text-muted-foreground hover:bg-accent hover:text-foreground"
}`} }`}
onClick={() => handleMenuClick(child)} onClick={() => handleMenuClick(child)}
@ -557,6 +581,28 @@ function AppLayoutInner({ children }: AppLayoutProps) {
const uiMenus = convertMenuToUI(currentMenus, user as ExtendedUserInfo); 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 ( return (
<div className="bg-background flex h-screen flex-col"> <div className="bg-background flex h-screen flex-col">
{/* 모바일 헤더 */} {/* 모바일 헤더 */}

View File

@ -5,6 +5,7 @@ import { apiClient } from "@/lib/api/client";
import { getCategoryValues } from "@/lib/api/tableCategoryValue"; import { getCategoryValues } from "@/lib/api/tableCategoryValue";
import { ChevronRight, FolderTree, Loader2, Search, X } from "lucide-react"; import { ChevronRight, FolderTree, Loader2, Search, X } from "lucide-react";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { cn } from "@/lib/utils";
export interface CategoryColumn { export interface CategoryColumn {
tableName: string; tableName: string;

View File

@ -26,7 +26,7 @@ function TabsList({
<TabsPrimitive.List <TabsPrimitive.List
data-slot="tabs-list" data-slot="tabs-list"
className={cn( 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 className
)} )}
{...props} {...props}
@ -42,7 +42,7 @@ function TabsTrigger({
<TabsPrimitive.Trigger <TabsPrimitive.Trigger
data-slot="tabs-trigger" data-slot="tabs-trigger"
className={cn( 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 className
)} )}
{...props} {...props}

View File

@ -3981,8 +3981,8 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
className={cn( className={cn(
"px-3 py-1 text-sm font-medium transition-colors", "px-3 py-1 text-sm font-medium transition-colors",
activeTabIndex === 0 activeTabIndex === 0
? "text-foreground border-b-2 border-primary" ? "text-primary border-b-2 border-primary font-semibold bg-primary/5"
: "text-muted-foreground hover:text-foreground" : "text-foreground/70 hover:text-foreground hover:bg-muted/30"
)} )}
> >
{componentConfig.rightPanel?.title || "기본"} {componentConfig.rightPanel?.title || "기본"}
@ -3994,8 +3994,8 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
className={cn( className={cn(
"px-3 py-1 text-sm font-medium transition-colors", "px-3 py-1 text-sm font-medium transition-colors",
activeTabIndex === index + 1 activeTabIndex === index + 1
? "text-foreground border-b-2 border-primary" ? "text-primary border-b-2 border-primary font-semibold bg-primary/5"
: "text-muted-foreground hover:text-foreground" : "text-foreground/70 hover:text-foreground hover:bg-muted/30"
)} )}
> >
{tab.label || `${index + 1}`} {tab.label || `${index + 1}`}

View File

@ -48,8 +48,8 @@ const TabsDesignEditor: React.FC<{
return cn( return cn(
"px-4 py-2 text-sm font-medium cursor-pointer transition-colors", "px-4 py-2 text-sm font-medium cursor-pointer transition-colors",
isActive isActive
? "bg-background border-b-2 border-primary text-primary" ? "bg-primary/10 border-b-2 border-primary text-primary font-semibold"
: "text-muted-foreground hover:text-foreground hover:bg-muted/50" : "text-foreground/70 hover:text-foreground hover:bg-muted/50"
); );
}; };
@ -283,7 +283,7 @@ const TabsDesignEditor: React.FC<{
return ( return (
<div className="flex h-full w-full flex-col overflow-hidden rounded-lg border bg-background"> <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.length > 0 ? (
tabs.map((tab) => ( tabs.map((tab) => (
<div <div
@ -649,8 +649,8 @@ ComponentRegistry.registerComponent({
return cn( return cn(
"px-4 py-2 text-sm font-medium cursor-pointer transition-colors", "px-4 py-2 text-sm font-medium cursor-pointer transition-colors",
isActive isActive
? "bg-background border-b-2 border-primary text-primary" ? "bg-primary/10 border-b-2 border-primary text-primary font-semibold"
: "text-muted-foreground hover:text-foreground hover:bg-muted/50" : "text-foreground/70 hover:text-foreground hover:bg-muted/50"
); );
}; };
@ -662,7 +662,7 @@ ComponentRegistry.registerComponent({
onDragEnd={onDragEnd} 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.length > 0 ? (
tabs.map((tab) => ( tabs.map((tab) => (
<div <div