feat(테이블타입): 컬럼 그리드 DBeaver 식 탭 분리 — 컬럼 / 참조
ColumnGrid 의 "참조/설정" 컬럼이 두 가지 다른 역할 (entity/code/numbering 의 참조 대상 표시 vs 그 외 타입의 default_value 표시) 을 한 셀에 욱여넣고 있었음. text/number/date 행에선 대부분 — 빈 칸. 진단 결과는 architect 가 동의한 대로 컬럼 통째 제거가 답. DBeaver 의 Columns / Foreign Keys 탭 분리 패턴 차용: - 컬럼 탭 (기본 활성) — 컬럼 본연의 속성만 (라벨 / 타입 / PK NN IDX UQ / ⋯). 참조/설정 컬럼 통째 제거, grid 6→5 컬럼으로 슬림화. 라벨 셀 폭 1fr 로 확보 - 참조 탭 — entity / code / category / numbering 컬럼만 모아 표 형태로. 종류별 그룹 헤더 + (소스 컬럼 / 참조 종류 / 참조 대상) 3컬럼 그리드. code 행에서 누락되어있던 코드 그룹 표시도 같이 채움 ReferenceListView 신규 컴포넌트로 분리. 사용자가 행 클릭하면 우측 상세 패널 슬라이드 in 하는 기존 동작은 양 탭 모두 동일. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -92,11 +92,10 @@ export function ColumnGrid({
|
||||
<div className="flex flex-1 flex-col overflow-hidden">
|
||||
<div
|
||||
className="grid flex-shrink-0 items-center border-b bg-muted/50 px-4 py-2 text-xs font-semibold text-foreground"
|
||||
style={{ gridTemplateColumns: "4px 140px 1fr 100px 160px 40px" }}
|
||||
style={{ gridTemplateColumns: "4px 1fr 100px 160px 40px" }}
|
||||
>
|
||||
<span />
|
||||
<span>라벨 · 컬럼명</span>
|
||||
<span>참조/설정</span>
|
||||
<span>타입</span>
|
||||
<span className="text-center">PK / NN / IDX / UQ</span>
|
||||
<span />
|
||||
@@ -142,7 +141,7 @@ export function ColumnGrid({
|
||||
}}
|
||||
className={cn(
|
||||
"grid min-h-12 cursor-pointer items-center gap-2 rounded-md border px-4 py-2 transition-colors",
|
||||
"grid-cols-[4px_140px_1fr_100px_160px_40px]",
|
||||
"grid-cols-[4px_1fr_100px_160px_40px]",
|
||||
"bg-card border-transparent hover:border-border hover:shadow-sm",
|
||||
isSelected && "border-primary/30 bg-primary/5 shadow-sm",
|
||||
)}
|
||||
@@ -159,66 +158,6 @@ export function ColumnGrid({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 참조/설정 칩 */}
|
||||
<div className="flex min-w-0 flex-wrap gap-1">
|
||||
{column.input_type === "entity" && column.reference_table && column.reference_table !== "none" && (
|
||||
<>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-xs font-normal"
|
||||
title={
|
||||
tables
|
||||
? (() => {
|
||||
const t = tables.find((tb) => tb.table_name === column.reference_table);
|
||||
return t?.display_name && t.display_name !== t.table_name
|
||||
? `${t.display_name} (${column.reference_table})`
|
||||
: column.reference_table;
|
||||
})()
|
||||
: column.reference_table
|
||||
}
|
||||
>
|
||||
{column.reference_table}
|
||||
</Badge>
|
||||
<span className="text-muted-foreground text-xs">→</span>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-xs font-normal"
|
||||
title={
|
||||
referenceTableColumns?.[column.reference_table]
|
||||
? (() => {
|
||||
const refCols = referenceTableColumns[column.reference_table];
|
||||
const c = refCols.find((rc) => rc.column_name === (column.reference_column ?? ""));
|
||||
return c?.display_name && c.display_name !== c.column_name
|
||||
? `${c.display_name} (${column.reference_column})`
|
||||
: column.reference_column ?? "—";
|
||||
})()
|
||||
: column.reference_column ?? "—"
|
||||
}
|
||||
>
|
||||
{column.reference_column || "—"}
|
||||
</Badge>
|
||||
</>
|
||||
)}
|
||||
{column.input_type === "code" && (
|
||||
<span className="text-muted-foreground truncate text-xs">
|
||||
{column.code_info ?? "—"} · {column.default_value ?? ""}
|
||||
</span>
|
||||
)}
|
||||
{column.input_type === "numbering" && column.numbering_rule_id && (
|
||||
<Badge variant="outline" className="text-xs font-normal">
|
||||
{column.numbering_rule_id}
|
||||
</Badge>
|
||||
)}
|
||||
{column.input_type !== "entity" &&
|
||||
column.input_type !== "code" &&
|
||||
column.input_type !== "numbering" &&
|
||||
(column.default_value ? (
|
||||
<span className="text-muted-foreground truncate text-xs">{column.default_value}</span>
|
||||
) : (
|
||||
<span className="text-muted-foreground/60 text-xs">—</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 타입 뱃지 */}
|
||||
<div className={cn("rounded-md border px-2 py-0.5 text-xs", typeConf.bgColor, typeConf.color)}>
|
||||
<span className="mr-1 inline-block h-1.5 w-1.5 rounded-full bg-current opacity-70" />
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
"use client";
|
||||
|
||||
import React, { useMemo } from "react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Database, FolderTree, Hash, Link2, FileCode2 } from "lucide-react";
|
||||
import type { ColumnTypeInfo, TableInfo } from "./types";
|
||||
import { INPUT_TYPE_COLORS } from "./types";
|
||||
import type { ReferenceTableColumn } from "@/lib/api/entityJoin";
|
||||
|
||||
export interface ReferenceListViewProps {
|
||||
columns: ColumnTypeInfo[];
|
||||
tables?: TableInfo[];
|
||||
referenceTableColumns?: Record<string, ReferenceTableColumn[]>;
|
||||
onSelectColumn?: (columnName: string) => void;
|
||||
selectedColumn?: string | null;
|
||||
}
|
||||
|
||||
type RefKind = "entity" | "code" | "category" | "numbering";
|
||||
|
||||
const KIND_META: Record<
|
||||
RefKind,
|
||||
{ icon: React.FC<{ className?: string }>; label: string; color: string; bgColor: string }
|
||||
> = {
|
||||
entity: { icon: Link2, label: "테이블 참조", color: "text-violet-600", bgColor: "bg-violet-50" },
|
||||
code: { icon: FileCode2, label: "공통코드", color: "text-emerald-600", bgColor: "bg-emerald-50" },
|
||||
category: { icon: FolderTree, label: "카테고리", color: "text-teal-600", bgColor: "bg-teal-50" },
|
||||
numbering: { icon: Hash, label: "채번", color: "text-orange-600", bgColor: "bg-orange-50" },
|
||||
};
|
||||
|
||||
function getRefKind(col: ColumnTypeInfo): RefKind | null {
|
||||
const t = col.input_type;
|
||||
if (t === "entity" || t === "code" || t === "category" || t === "numbering") return t;
|
||||
return null;
|
||||
}
|
||||
|
||||
export function ReferenceListView({
|
||||
columns,
|
||||
tables,
|
||||
referenceTableColumns,
|
||||
onSelectColumn,
|
||||
selectedColumn = null,
|
||||
}: ReferenceListViewProps) {
|
||||
const grouped = useMemo(() => {
|
||||
const groups: Record<RefKind, ColumnTypeInfo[]> = {
|
||||
entity: [],
|
||||
code: [],
|
||||
category: [],
|
||||
numbering: [],
|
||||
};
|
||||
for (const col of columns) {
|
||||
const kind = getRefKind(col);
|
||||
if (kind) groups[kind].push(col);
|
||||
}
|
||||
return groups;
|
||||
}, [columns]);
|
||||
|
||||
const totalRefs =
|
||||
grouped.entity.length + grouped.code.length + grouped.category.length + grouped.numbering.length;
|
||||
|
||||
if (totalRefs === 0) {
|
||||
return (
|
||||
<div className="flex flex-1 items-center justify-center py-12 text-sm text-muted-foreground">
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<Database className="h-8 w-8 text-muted-foreground/50" />
|
||||
<span>이 테이블에는 참조 컬럼이 없어요.</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-1 flex-col overflow-hidden">
|
||||
{/* 헤더 */}
|
||||
<div
|
||||
className="grid flex-shrink-0 items-center border-b bg-muted/50 px-4 py-2 text-xs font-semibold text-foreground"
|
||||
style={{ gridTemplateColumns: "4px 220px 110px 1fr" }}
|
||||
>
|
||||
<span />
|
||||
<span>소스 컬럼</span>
|
||||
<span>참조 종류</span>
|
||||
<span>참조 대상</span>
|
||||
</div>
|
||||
|
||||
{/* 그룹별 행 */}
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{(["entity", "code", "category", "numbering"] as const).map((kind) => {
|
||||
const list = grouped[kind];
|
||||
if (list.length === 0) return null;
|
||||
const meta = KIND_META[kind];
|
||||
const KindIcon = meta.icon;
|
||||
return (
|
||||
<div key={kind} className="space-y-1 py-2">
|
||||
<div className="flex items-center gap-2 border-b border-border/60 px-4 pb-1.5">
|
||||
<KindIcon className={cn("h-4 w-4", meta.color)} />
|
||||
<span className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
||||
{meta.label}
|
||||
</span>
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{list.length}
|
||||
</Badge>
|
||||
</div>
|
||||
{list.map((column) => {
|
||||
const typeConf = INPUT_TYPE_COLORS[column.input_type || "text"] || INPUT_TYPE_COLORS.text;
|
||||
const isSelected = selectedColumn === column.column_name;
|
||||
return (
|
||||
<div
|
||||
key={column.column_name}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => onSelectColumn?.(column.column_name)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault();
|
||||
onSelectColumn?.(column.column_name);
|
||||
}
|
||||
}}
|
||||
className={cn(
|
||||
"grid min-h-10 cursor-pointer items-center gap-2 rounded-md border px-4 py-2 transition-colors",
|
||||
"bg-card border-transparent hover:border-border hover:shadow-sm",
|
||||
isSelected && "border-primary/30 bg-primary/5 shadow-sm",
|
||||
)}
|
||||
style={{ gridTemplateColumns: "4px 220px 110px 1fr" }}
|
||||
>
|
||||
{/* 색상바 */}
|
||||
<div className={cn("h-full min-h-8 w-1 rounded-full", typeConf.barColor)} />
|
||||
|
||||
{/* 소스 컬럼명 */}
|
||||
<div className="min-w-0">
|
||||
<div className="truncate text-xs font-medium">
|
||||
{column.display_name && column.display_name !== column.column_name
|
||||
? `${column.display_name} (${column.column_name})`
|
||||
: column.column_name}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 참조 종류 칩 */}
|
||||
<div className={cn("inline-flex w-fit items-center gap-1 rounded-md border px-2 py-0.5 text-xs", meta.bgColor, meta.color)}>
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-current opacity-70" />
|
||||
{meta.label}
|
||||
</div>
|
||||
|
||||
{/* 참조 대상 */}
|
||||
<div className="flex min-w-0 flex-wrap items-center gap-1">
|
||||
{kind === "entity" && column.reference_table && column.reference_table !== "none" ? (
|
||||
<>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-xs font-normal"
|
||||
title={
|
||||
tables
|
||||
? (() => {
|
||||
const t = tables.find((tb) => tb.table_name === column.reference_table);
|
||||
return t?.display_name && t.display_name !== t.table_name
|
||||
? `${t.display_name} (${column.reference_table})`
|
||||
: column.reference_table;
|
||||
})()
|
||||
: column.reference_table
|
||||
}
|
||||
>
|
||||
{column.reference_table}
|
||||
</Badge>
|
||||
<span className="text-muted-foreground text-xs">→</span>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-xs font-normal"
|
||||
title={
|
||||
referenceTableColumns?.[column.reference_table]
|
||||
? (() => {
|
||||
const refCols = referenceTableColumns[column.reference_table];
|
||||
const c = refCols.find((rc) => rc.column_name === (column.reference_column ?? ""));
|
||||
return c?.display_name && c.display_name !== c.column_name
|
||||
? `${c.display_name} (${column.reference_column})`
|
||||
: column.reference_column ?? "—";
|
||||
})()
|
||||
: column.reference_column ?? "—"
|
||||
}
|
||||
>
|
||||
{column.reference_column || "—"}
|
||||
</Badge>
|
||||
</>
|
||||
) : kind === "code" ? (
|
||||
column.code_info ? (
|
||||
<Badge variant="outline" className="text-xs font-normal">
|
||||
코드: {column.code_info}
|
||||
</Badge>
|
||||
) : (
|
||||
<span className="text-muted-foreground/60 text-xs">— (코드 그룹 미지정)</span>
|
||||
)
|
||||
) : kind === "category" ? (
|
||||
column.category_ref ? (
|
||||
<Badge variant="outline" className="text-xs font-normal">
|
||||
카테고리: {column.category_ref}
|
||||
</Badge>
|
||||
) : column.category_menus && column.category_menus.length > 0 ? (
|
||||
<Badge variant="outline" className="text-xs font-normal">
|
||||
카테고리 메뉴 {column.category_menus.length}개
|
||||
</Badge>
|
||||
) : (
|
||||
<span className="text-muted-foreground/60 text-xs">— (카테고리 미지정)</span>
|
||||
)
|
||||
) : kind === "numbering" ? (
|
||||
column.numbering_rule_id ? (
|
||||
<Badge variant="outline" className="text-xs font-normal">
|
||||
채번: {column.numbering_rule_id}
|
||||
</Badge>
|
||||
) : (
|
||||
<span className="text-muted-foreground/60 text-xs">— (채번 규칙 미지정)</span>
|
||||
)
|
||||
) : (
|
||||
<span className="text-muted-foreground/60 text-xs">—</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user