라벨 · 컬럼명
- 참조/설정
타입
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}
+
+ ) : (
+ — (채번 규칙 미지정)
+ )
+ ) : (
+ —
+ )}
+
+
+ );
+ })}
+
+ );
+ })}
+
+
+ );
+}
diff --git a/frontend/components/admin/table-type/TypeOverviewStrip.tsx b/frontend/components/admin/table-type/TypeOverviewStrip.tsx
index c63ed1f3..19474c0e 100644
--- a/frontend/components/admin/table-type/TypeOverviewStrip.tsx
+++ b/frontend/components/admin/table-type/TypeOverviewStrip.tsx
@@ -3,7 +3,7 @@
import React, { useMemo } from "react";
import { cn } from "@/lib/utils";
import type { ColumnTypeInfo } from "./types";
-import { INPUT_TYPE_COLORS } from "./types";
+import { INPUT_TYPE_COLORS, FALLBACK_TYPE_CONFIG } from "./types";
import { USER_SELECTABLE_INPUT_TYPE_ORDER } from "@/types/input-types";
export interface TypeOverviewStripProps {
@@ -57,20 +57,13 @@ export function TypeOverviewStrip({
/** stroke-dasharray: 비율만큼 둘레에 할당 (둘레 100 기준) */
const circumference = 100;
let offset = 0;
- const LEGACY_CONF = {
- color: "text-amber-600",
- bgColor: "bg-amber-50",
- barColor: "bg-amber-400",
- label: "Legacy",
- desc: "구버전 타입",
- iconChar: "?",
- };
+ const LEGACY_CONF = { ...FALLBACK_TYPE_CONFIG, color: "text-amber-600", bgColor: "bg-amber-50", barColor: "bg-amber-400" };
const segmentPaths = segments.map(({ type, ratio, isLegacy }) => {
const length = ratio * circumference;
const dashArray = `${length} ${circumference - length}`;
const dashOffset = -offset;
offset += length;
- const conf = isLegacy ? LEGACY_CONF : (INPUT_TYPE_COLORS[type] || { color: "text-muted-foreground", bgColor: "bg-muted" });
+ const conf = isLegacy ? LEGACY_CONF : (INPUT_TYPE_COLORS[type] || FALLBACK_TYPE_CONFIG);
return {
type,
dashArray,
@@ -112,7 +105,7 @@ export function TypeOverviewStrip({
.filter((type) => (counts[type] || 0) > 0)
.sort((a, b) => (counts[b] ?? 0) - (counts[a] ?? 0))
.map((type) => {
- const conf = INPUT_TYPE_COLORS[type] || { color: "text-muted-foreground", bgColor: "bg-muted", label: type };
+ const conf = INPUT_TYPE_COLORS[type] || { ...FALLBACK_TYPE_CONFIG, label: type };
const isActive = activeFilter === null || activeFilter === type;
return (