From 2d39d174281ee9f96ff48e7fd7cff8e59c50eee1 Mon Sep 17 00:00:00 2001 From: johngreen Date: Thu, 21 May 2026 19:21:55 +0900 Subject: [PATCH] =?UTF-8?q?feat(=ED=85=8C=EC=9D=B4=EB=B8=94=ED=83=80?= =?UTF-8?q?=EC=9E=85):=20=EC=BB=AC=EB=9F=BC=20=EA=B7=B8=EB=A6=AC=EB=93=9C?= =?UTF-8?q?=20DBeaver=20=EC=8B=9D=20=ED=83=AD=20=EB=B6=84=EB=A6=AC=20?= =?UTF-8?q?=E2=80=94=20=EC=BB=AC=EB=9F=BC=20/=20=EC=B0=B8=EC=A1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../admin/systemMng/tableMngList/page.tsx | 111 ++++++--- .../admin/table-type/ColumnGrid.tsx | 65 +---- .../admin/table-type/ReferenceListView.tsx | 223 ++++++++++++++++++ 3 files changed, 299 insertions(+), 100 deletions(-) create mode 100644 frontend/components/admin/table-type/ReferenceListView.tsx diff --git a/frontend/app/(main)/admin/systemMng/tableMngList/page.tsx b/frontend/app/(main)/admin/systemMng/tableMngList/page.tsx index ec166cbc..4118f898 100644 --- a/frontend/app/(main)/admin/systemMng/tableMngList/page.tsx +++ b/frontend/app/(main)/admin/systemMng/tableMngList/page.tsx @@ -21,7 +21,10 @@ import { ChevronsUpDown, Loader2, Pencil, + Columns3, + Link2, } from "lucide-react"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { cn } from "@/lib/utils"; import { LoadingSpinner } from "@/components/common/LoadingSpinner"; import { toast } from "sonner"; @@ -56,6 +59,7 @@ import type { TableInfo, ColumnTypeInfo, SecondLevelMenu } from "@/components/ad import { TypeOverviewStrip } from "@/components/admin/table-type/TypeOverviewStrip"; import { ColumnGrid } from "@/components/admin/table-type/ColumnGrid"; import { ColumnDetailPanel } from "@/components/admin/table-type/ColumnDetailPanel"; +import { ReferenceListView } from "@/components/admin/table-type/ReferenceListView"; export default function TableManagementPage() { const { userLang, getText } = useMultiLang({ companyCode: "*" }); @@ -1690,46 +1694,79 @@ export default function TableManagementPage() { {getTextFromUI(TABLE_MANAGEMENT_KEYS.MESSAGE_NO_COLUMNS, "컬럼이 없습니다")} ) : ( - <> - - setSelectedColumn((prev) => (prev === c ? null : c))} - onColumnChange={(columnName, field, value) => { - if (field === "is_unique") { - const currentColumn = columns.find((c) => c.column_name === columnName); - if (currentColumn) { - handleUniqueToggle(columnName, currentColumn.is_unique || "NO"); + + + + + 컬럼 + + + + 참조 + {(() => { + const refCount = columns.filter((c) => + ["entity", "code", "category", "numbering"].includes(c.input_type), + ).length; + return refCount > 0 ? ( + + {refCount} + + ) : null; + })()} + + + + + + setSelectedColumn((prev) => (prev === c ? null : c))} + onColumnChange={(columnName, field, value) => { + if (field === "is_unique") { + const currentColumn = columns.find((c) => c.column_name === columnName); + if (currentColumn) { + handleUniqueToggle(columnName, currentColumn.is_unique || "NO"); + } + return; } - return; - } - if (field === "is_nullable") { - const currentColumn = columns.find((c) => c.column_name === columnName); - if (currentColumn) { - handleNullableToggle(columnName, currentColumn.is_nullable || "YES"); + if (field === "is_nullable") { + const currentColumn = columns.find((c) => c.column_name === columnName); + if (currentColumn) { + handleNullableToggle(columnName, currentColumn.is_nullable || "YES"); + } + return; } - return; + const idx = columns.findIndex((c) => c.column_name === columnName); + if (idx >= 0) handleColumnChange(idx, field, value); + }} + constraints={constraints} + typeFilter={typeFilter} + getColumnIndexState={getColumnIndexState} + onPkToggle={handlePkToggle} + onIndexToggle={(columnName, checked) => + handleIndexToggle(columnName, "index", checked) } - const idx = columns.findIndex((c) => c.column_name === columnName); - if (idx >= 0) handleColumnChange(idx, field, value); - }} - constraints={constraints} - typeFilter={typeFilter} - getColumnIndexState={getColumnIndexState} - onPkToggle={handlePkToggle} - onIndexToggle={(columnName, checked) => - handleIndexToggle(columnName, "index", checked) - } - onDeleteColumn={handleDeleteColumnClick} - tables={tables} - referenceTableColumns={referenceTableColumns} - /> - + onDeleteColumn={handleDeleteColumnClick} + tables={tables} + referenceTableColumns={referenceTableColumns} + /> + + + + setSelectedColumn((prev) => (prev === c ? null : c))} + /> + + )} )} diff --git a/frontend/components/admin/table-type/ColumnGrid.tsx b/frontend/components/admin/table-type/ColumnGrid.tsx index e2f5610e..bbd76f4b 100644 --- a/frontend/components/admin/table-type/ColumnGrid.tsx +++ b/frontend/components/admin/table-type/ColumnGrid.tsx @@ -92,11 +92,10 @@ export function ColumnGrid({
라벨 · 컬럼명 - 참조/설정 타입 PK / NN / IDX / UQ @@ -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({
- {/* 참조/설정 칩 */} -
- {column.input_type === "entity" && column.reference_table && column.reference_table !== "none" && ( - <> - { - 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} - - - { - 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 || "—"} - - - )} - {column.input_type === "code" && ( - - {column.code_info ?? "—"} · {column.default_value ?? ""} - - )} - {column.input_type === "numbering" && column.numbering_rule_id && ( - - {column.numbering_rule_id} - - )} - {column.input_type !== "entity" && - column.input_type !== "code" && - column.input_type !== "numbering" && - (column.default_value ? ( - {column.default_value} - ) : ( - - ))} -
- {/* 타입 뱃지 */}
diff --git a/frontend/components/admin/table-type/ReferenceListView.tsx b/frontend/components/admin/table-type/ReferenceListView.tsx new file mode 100644 index 00000000..5bca69e5 --- /dev/null +++ b/frontend/components/admin/table-type/ReferenceListView.tsx @@ -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; + 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 = { + 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 ( +
+
+ + 이 테이블에는 참조 컬럼이 없어요. +
+
+ ); + } + + return ( +
+ {/* 헤더 */} +
+ + 소스 컬럼 + 참조 종류 + 참조 대상 +
+ + {/* 그룹별 행 */} +
+ {(["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 ( +
+
+ + + {meta.label} + + + {list.length} + +
+ {list.map((column) => { + const typeConf = INPUT_TYPE_COLORS[column.input_type || "text"] || INPUT_TYPE_COLORS.text; + const isSelected = selectedColumn === column.column_name; + return ( +
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" }} + > + {/* 색상바 */} +
+ + {/* 소스 컬럼명 */} +
+
+ {column.display_name && column.display_name !== column.column_name + ? `${column.display_name} (${column.column_name})` + : column.column_name} +
+
+ + {/* 참조 종류 칩 */} +
+ + {meta.label} +
+ + {/* 참조 대상 */} +
+ {kind === "entity" && column.reference_table && column.reference_table !== "none" ? ( + <> + { + 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} + + + { + 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 || "—"} + + + ) : kind === "code" ? ( + column.code_info ? ( + + 코드: {column.code_info} + + ) : ( + — (코드 그룹 미지정) + ) + ) : kind === "category" ? ( + column.category_ref ? ( + + 카테고리: {column.category_ref} + + ) : column.category_menus && column.category_menus.length > 0 ? ( + + 카테고리 메뉴 {column.category_menus.length}개 + + ) : ( + — (카테고리 미지정) + ) + ) : kind === "numbering" ? ( + column.numbering_rule_id ? ( + + 채번: {column.numbering_rule_id} + + ) : ( + — (채번 규칙 미지정) + ) + ) : ( + + )} +
+
+ ); + })} +
+ ); + })} +
+
+ ); +}