ERP-node/frontend/components/screen/TableSettingModal.tsx

1095 lines
34 KiB
TypeScript
Raw Normal View History

"use client";
import React, { useState, useEffect, useCallback, useMemo } from "react";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Badge } from "@/components/ui/badge";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command";
import { toast } from "sonner";
import { cn } from "@/lib/utils";
import {
Database,
Link2,
Columns3,
Key,
Save,
Plus,
Pencil,
Trash2,
RefreshCw,
Loader2,
Check,
ChevronsUpDown,
Table2,
ArrowRight,
Eye,
Settings2,
} from "lucide-react";
import {
getFieldJoins,
createFieldJoin,
updateFieldJoin,
deleteFieldJoin,
FieldJoin,
} from "@/lib/api/screenGroup";
import { tableManagementApi, ColumnTypeInfo, TableInfo } from "@/lib/api/tableManagement";
// ============================================================
// 타입 정의
// ============================================================
interface JoinColumnRef {
column: string;
refTable: string;
refTableLabel?: string;
refColumn: string;
}
interface ReferencedBy {
fromTable: string;
fromTableLabel?: string;
fromColumn: string;
toColumn: string;
}
interface ColumnInfo {
column: string;
label?: string;
type?: string;
isPK?: boolean;
isFK?: boolean;
refTable?: string;
refColumn?: string;
}
interface TableSettingModalProps {
isOpen: boolean;
onClose: () => void;
tableName: string;
tableLabel?: string;
screenId?: number;
joinColumnRefs?: JoinColumnRef[];
referencedBy?: ReferencedBy[];
columns?: ColumnInfo[];
filterColumns?: string[];
onSaveSuccess?: () => void;
}
// 검색 가능한 Select 컴포넌트
interface SearchableSelectProps {
value: string;
onValueChange: (value: string) => void;
options: Array<{ value: string; label: string; description?: string }>;
placeholder?: string;
disabled?: boolean;
className?: string;
}
function SearchableSelect({
value,
onValueChange,
options,
placeholder = "선택...",
disabled = false,
className,
}: SearchableSelectProps) {
const [open, setOpen] = useState(false);
const selectedOption = options.find((opt) => opt.value === value);
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
disabled={disabled}
className={cn("h-8 w-full justify-between text-xs", className)}
>
{selectedOption ? (
<span className="truncate">{selectedOption.label}</span>
) : (
<span className="text-muted-foreground">{placeholder}</span>
)}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent
className="p-0"
style={{ width: "var(--radix-popover-trigger-width)" }}
align="start"
>
<Command>
<CommandInput placeholder="검색..." className="text-xs" />
<CommandList>
<CommandEmpty className="py-2 text-center text-xs">
</CommandEmpty>
<CommandGroup>
{options.map((option) => (
<CommandItem
key={option.value}
value={option.value}
onSelect={() => {
onValueChange(option.value);
setOpen(false);
}}
className="text-xs"
>
<Check
className={cn(
"mr-2 h-4 w-4",
value === option.value ? "opacity-100" : "opacity-0"
)}
/>
<div className="flex flex-col">
<span>{option.label}</span>
{option.description && (
<span className="text-muted-foreground text-[10px]">
{option.description}
</span>
)}
</div>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
}
// ============================================================
// 메인 모달 컴포넌트
// ============================================================
export function TableSettingModal({
isOpen,
onClose,
tableName,
tableLabel,
screenId,
joinColumnRefs = [],
referencedBy = [],
columns = [],
filterColumns = [],
onSaveSuccess,
}: TableSettingModalProps) {
const [activeTab, setActiveTab] = useState("info");
const [loading, setLoading] = useState(false);
const [tableColumns, setTableColumns] = useState<ColumnTypeInfo[]>([]);
const [tables, setTables] = useState<TableInfo[]>([]);
const [fieldJoins, setFieldJoins] = useState<FieldJoin[]>([]);
// 테이블 컬럼 정보 로드
const loadTableColumns = useCallback(async () => {
if (!tableName) return;
setLoading(true);
try {
// 테이블 목록 로드
const tablesResponse = await tableManagementApi.getTables();
if (tablesResponse.success && tablesResponse.data) {
setTables(tablesResponse.data);
}
// 테이블 컬럼 로드
const columnsResponse = await tableManagementApi.getTableColumns(tableName);
if (columnsResponse.success && columnsResponse.data) {
setTableColumns(columnsResponse.data);
}
// 필드 조인 로드 (screenId가 있는 경우)
if (screenId) {
const joinsResponse = await getFieldJoins(screenId);
if (joinsResponse.success && joinsResponse.data) {
// 이 테이블과 관련된 조인만 필터링
const relevantJoins = joinsResponse.data.filter(
(j) => j.save_table === tableName || j.join_table === tableName
);
setFieldJoins(relevantJoins);
}
}
} catch (error) {
console.error("테이블 정보 로드 실패:", error);
toast.error("테이블 정보를 불러오는데 실패했습니다.");
} finally {
setLoading(false);
}
}, [tableName, screenId]);
useEffect(() => {
if (isOpen && tableName) {
loadTableColumns();
}
}, [isOpen, tableName, loadTableColumns]);
// 새로고침
const handleRefresh = () => {
loadTableColumns();
toast.success("새로고침 완료");
};
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="flex h-[80vh] max-h-[800px] w-[95vw] max-w-[1000px] flex-col">
<DialogHeader className="flex-shrink-0">
<DialogTitle className="flex items-center gap-2 text-lg">
<Table2 className="h-5 w-5 text-green-500" />
: {tableLabel || tableName}
</DialogTitle>
<DialogDescription className="text-sm">
, , .
{tableName !== tableLabel && (
<span className="ml-2 text-muted-foreground">({tableName})</span>
)}
</DialogDescription>
</DialogHeader>
<Tabs
value={activeTab}
onValueChange={setActiveTab}
className="flex min-h-0 flex-1 flex-col"
>
<div className="flex flex-shrink-0 items-center justify-between border-b pb-2">
<TabsList className="h-9">
<TabsTrigger value="info" className="gap-1 text-xs">
<Columns3 className="h-3.5 w-3.5" />
</TabsTrigger>
<TabsTrigger value="joins" className="gap-1 text-xs">
<Link2 className="h-3.5 w-3.5" />
</TabsTrigger>
<TabsTrigger value="references" className="gap-1 text-xs">
<Eye className="h-3.5 w-3.5" />
</TabsTrigger>
</TabsList>
<Button
variant="outline"
size="sm"
onClick={handleRefresh}
className="gap-1"
>
<RefreshCw className={cn("h-4 w-4", loading && "animate-spin")} />
</Button>
</div>
{/* 탭 1: 테이블 정보 */}
<TabsContent value="info" className="mt-0 min-h-0 flex-1 overflow-auto p-4">
<TableInfoTab
tableName={tableName}
tableLabel={tableLabel}
columns={columns}
tableColumns={tableColumns}
filterColumns={filterColumns}
loading={loading}
/>
</TabsContent>
{/* 탭 2: 조인 설정 */}
<TabsContent value="joins" className="mt-0 min-h-0 flex-1 overflow-auto p-4">
<JoinSettingTab
tableName={tableName}
tableLabel={tableLabel}
screenId={screenId}
joinColumnRefs={joinColumnRefs}
fieldJoins={fieldJoins}
tables={tables}
tableColumns={tableColumns}
loading={loading}
onReload={loadTableColumns}
onSaveSuccess={onSaveSuccess}
/>
</TabsContent>
{/* 탭 3: 참조 관계 */}
<TabsContent value="references" className="mt-0 min-h-0 flex-1 overflow-auto p-4">
<ReferenceTab
tableName={tableName}
tableLabel={tableLabel}
referencedBy={referencedBy}
joinColumnRefs={joinColumnRefs}
loading={loading}
/>
</TabsContent>
</Tabs>
</DialogContent>
</Dialog>
);
}
// ============================================================
// 탭 1: 테이블 정보
// ============================================================
interface TableInfoTabProps {
tableName: string;
tableLabel?: string;
columns: ColumnInfo[];
tableColumns: ColumnTypeInfo[];
filterColumns: string[];
loading: boolean;
}
function TableInfoTab({
tableName,
tableLabel,
columns,
tableColumns,
filterColumns,
loading,
}: TableInfoTabProps) {
// 컬럼 정보 통합 (기존 columns + API에서 가져온 tableColumns)
const mergedColumns = useMemo(() => {
const columnsMap = new Map<string, ColumnInfo>();
// 먼저 기존 columns 추가
columns.forEach((col) => {
columnsMap.set(col.column, col);
});
// API에서 가져온 컬럼 정보로 보강
tableColumns.forEach((tcol) => {
const existing = columnsMap.get(tcol.column_name);
if (existing) {
columnsMap.set(tcol.column_name, {
...existing,
type: tcol.data_type,
isPK: tcol.is_primary_key,
isFK: tcol.is_foreign_key,
refTable: tcol.references?.table,
refColumn: tcol.references?.column,
label: existing.label || tcol.column_name,
});
} else {
columnsMap.set(tcol.column_name, {
column: tcol.column_name,
label: tcol.column_name,
type: tcol.data_type,
isPK: tcol.is_primary_key,
isFK: tcol.is_foreign_key,
refTable: tcol.references?.table,
refColumn: tcol.references?.column,
});
}
});
return Array.from(columnsMap.values());
}, [columns, tableColumns]);
// PK, FK 분류
const pkColumns = mergedColumns.filter((c) => c.isPK);
const fkColumns = mergedColumns.filter((c) => c.isFK);
if (loading) {
return (
<div className="flex h-64 items-center justify-center">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
);
}
return (
<div className="space-y-6">
{/* 기본 정보 */}
<div className="grid grid-cols-2 gap-4 md:grid-cols-4">
<div className="rounded-lg border bg-blue-50 p-4">
<div className="text-2xl font-bold text-blue-600">{mergedColumns.length}</div>
<div className="text-xs text-blue-700"> </div>
</div>
<div className="rounded-lg border bg-orange-50 p-4">
<div className="text-2xl font-bold text-orange-600">{pkColumns.length}</div>
<div className="text-xs text-orange-700">Primary Key</div>
</div>
<div className="rounded-lg border bg-green-50 p-4">
<div className="text-2xl font-bold text-green-600">{fkColumns.length}</div>
<div className="text-xs text-green-700">Foreign Key</div>
</div>
<div className="rounded-lg border bg-purple-50 p-4">
<div className="text-2xl font-bold text-purple-600">{filterColumns.length}</div>
<div className="text-xs text-purple-700"> </div>
</div>
</div>
{/* 필터 컬럼 */}
{filterColumns.length > 0 && (
<div className="space-y-2">
<h3 className="text-sm font-semibold"> </h3>
<div className="flex flex-wrap gap-2">
{filterColumns.map((col, idx) => (
<Badge key={idx} variant="outline" className="bg-purple-50 text-purple-700">
{col}
</Badge>
))}
</div>
</div>
)}
{/* 컬럼 목록 */}
<div className="space-y-2">
<h3 className="text-sm font-semibold"> </h3>
<div className="rounded-lg border">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[50px] text-xs">#</TableHead>
<TableHead className="text-xs"></TableHead>
<TableHead className="text-xs"> </TableHead>
<TableHead className="text-xs"></TableHead>
<TableHead className="text-xs"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{mergedColumns.length === 0 ? (
<TableRow>
<TableCell
colSpan={5}
className="py-8 text-center text-sm text-muted-foreground"
>
.
</TableCell>
</TableRow>
) : (
mergedColumns.map((col, idx) => (
<TableRow
key={col.column}
className={cn(
col.isPK && "bg-orange-50/50",
col.isFK && "bg-green-50/50"
)}
>
<TableCell className="text-xs text-muted-foreground">
{idx + 1}
</TableCell>
<TableCell className="text-xs font-medium">
{col.label || col.column}
{col.label && col.column !== col.label && (
<span className="ml-1 text-muted-foreground">({col.column})</span>
)}
</TableCell>
<TableCell className="text-xs text-muted-foreground">
{col.type || "-"}
</TableCell>
<TableCell>
<div className="flex gap-1">
{col.isPK && (
<Badge variant="outline" className="bg-orange-100 text-orange-700 text-[10px]">
<Key className="mr-1 h-3 w-3" />
PK
</Badge>
)}
{col.isFK && (
<Badge variant="outline" className="bg-green-100 text-green-700 text-[10px]">
<Link2 className="mr-1 h-3 w-3" />
FK
</Badge>
)}
</div>
</TableCell>
<TableCell className="text-xs text-muted-foreground">
{col.refTable && col.refColumn ? (
<span className="flex items-center gap-1">
<ArrowRight className="h-3 w-3" />
{col.refTable}.{col.refColumn}
</span>
) : (
"-"
)}
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
</div>
</div>
);
}
// ============================================================
// 탭 2: 조인 설정
// ============================================================
interface JoinSettingTabProps {
tableName: string;
tableLabel?: string;
screenId?: number;
joinColumnRefs: JoinColumnRef[];
fieldJoins: FieldJoin[];
tables: TableInfo[];
tableColumns: ColumnTypeInfo[];
loading: boolean;
onReload: () => void;
onSaveSuccess?: () => void;
}
function JoinSettingTab({
tableName,
tableLabel,
screenId,
joinColumnRefs,
fieldJoins,
tables,
tableColumns,
loading,
onReload,
onSaveSuccess,
}: JoinSettingTabProps) {
const [isEditing, setIsEditing] = useState(false);
const [editItem, setEditItem] = useState<FieldJoin | null>(null);
const [formData, setFormData] = useState({
save_column: "",
join_table: "",
join_column: "",
display_column: "",
join_type: "LEFT",
});
const [targetColumns, setTargetColumns] = useState<ColumnTypeInfo[]>([]);
// 조인 테이블 변경 시 컬럼 로드
const loadTargetColumns = useCallback(async (targetTable: string) => {
if (!targetTable) {
setTargetColumns([]);
return;
}
try {
const response = await tableManagementApi.getTableColumns(targetTable);
if (response.success && response.data) {
setTargetColumns(response.data);
}
} catch (error) {
console.error("대상 테이블 컬럼 로드 실패:", error);
}
}, []);
useEffect(() => {
if (formData.join_table) {
loadTargetColumns(formData.join_table);
}
}, [formData.join_table, loadTargetColumns]);
// 폼 초기화
const resetForm = () => {
setFormData({
save_column: "",
join_table: "",
join_column: "",
display_column: "",
join_type: "LEFT",
});
setEditItem(null);
setIsEditing(false);
setTargetColumns([]);
};
// 수정 모드
const handleEdit = (item: FieldJoin) => {
setEditItem(item);
setFormData({
save_column: item.save_column,
join_table: item.join_table,
join_column: item.join_column,
display_column: item.display_column || "",
join_type: item.join_type,
});
setIsEditing(true);
loadTargetColumns(item.join_table);
};
// 디자이너 설정을 DB로 저장
const handleSaveDesignerJoin = async (join: JoinColumnRef) => {
if (!screenId) {
toast.error("화면 ID가 필요합니다.");
return;
}
try {
const payload = {
screen_id: screenId,
save_table: tableName,
save_column: join.column,
join_table: join.refTable,
join_column: join.refColumn,
display_column: "",
join_type: "LEFT",
is_active: "Y",
};
const response = await createFieldJoin(payload);
if (response.success) {
toast.success("조인 설정이 DB에 저장되었습니다.");
onReload();
onSaveSuccess?.();
} else {
toast.error(response.message || "저장에 실패했습니다.");
}
} catch (error) {
console.error("저장 오류:", error);
toast.error("저장 중 오류가 발생했습니다.");
}
};
// 저장
const handleSave = async () => {
if (!screenId) {
toast.error("화면 ID가 필요합니다.");
return;
}
if (!formData.save_column || !formData.join_table || !formData.join_column) {
toast.error("필수 항목을 모두 입력해주세요.");
return;
}
try {
const payload = {
screen_id: screenId,
save_table: tableName,
save_column: formData.save_column,
join_table: formData.join_table,
join_column: formData.join_column,
display_column: formData.display_column || null,
join_type: formData.join_type,
is_active: "Y",
};
let response;
if (editItem) {
response = await updateFieldJoin(editItem.id, payload);
} else {
response = await createFieldJoin(payload);
}
if (response.success) {
toast.success(editItem ? "조인 설정이 수정되었습니다." : "조인 설정이 추가되었습니다.");
resetForm();
onReload();
onSaveSuccess?.();
} else {
toast.error(response.message || "저장에 실패했습니다.");
}
} catch (error) {
console.error("저장 오류:", error);
toast.error("저장 중 오류가 발생했습니다.");
}
};
// 삭제
const handleDelete = async (id: number) => {
if (!confirm("정말로 삭제하시겠습니까?")) return;
try {
const response = await deleteFieldJoin(id);
if (response.success) {
toast.success("조인 설정이 삭제되었습니다.");
onReload();
onSaveSuccess?.();
} else {
toast.error(response.message || "삭제에 실패했습니다.");
}
} catch (error) {
console.error("삭제 오류:", error);
toast.error("삭제 중 오류가 발생했습니다.");
}
};
// 통합 조인 목록 (디자이너 + DB)
const unifiedJoins = useMemo(() => {
// DB에서 가져온 조인
const dbJoins = fieldJoins.map((j) => ({
...j,
id: j.id,
source: "db" as const,
}));
// 디자이너 조인 (DB에 없는 것만)
const dbJoinKeys = new Set(
fieldJoins.map((j) => `${j.save_column}:${j.join_table}:${j.join_column}`)
);
const designerJoins = joinColumnRefs
.filter(
(j) => !dbJoinKeys.has(`${j.column}:${j.refTable}:${j.refColumn}`)
)
.map((j, idx) => ({
id: `designer-${idx}`,
source: "designer" as const,
save_table: tableName,
save_column: j.column,
join_table: j.refTable,
join_table_label: j.refTableLabel,
join_column: j.refColumn,
display_column: "",
join_type: "LEFT",
}));
return [...designerJoins, ...dbJoins];
}, [fieldJoins, joinColumnRefs, tableName]);
// 테이블 옵션
const tableOptions = useMemo(
() =>
tables.map((t) => ({
value: t.table_name,
label: t.table_name,
})),
[tables]
);
// 현재 테이블 컬럼 옵션
const columnOptions = useMemo(
() =>
tableColumns.map((c) => ({
value: c.column_name,
label: c.column_name,
description: c.data_type,
})),
[tableColumns]
);
// 대상 테이블 컬럼 옵션
const targetColumnOptions = useMemo(
() =>
targetColumns.map((c) => ({
value: c.column_name,
label: c.column_name,
description: c.data_type,
})),
[targetColumns]
);
if (loading) {
return (
<div className="flex h-64 items-center justify-center">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
);
}
return (
<div className="space-y-4">
{/* 입력 폼 */}
{screenId && (
<div className="space-y-3 rounded-lg bg-muted/50 p-4">
<div className="text-sm font-medium">
{isEditing ? "조인 설정 수정" : "새 조인 설정 추가"}
</div>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-5">
<div>
<Label className="text-xs"> *</Label>
<SearchableSelect
value={formData.save_column}
onValueChange={(v) => setFormData({ ...formData, save_column: v })}
options={columnOptions}
placeholder="컬럼 선택"
/>
</div>
<div>
<Label className="text-xs"> *</Label>
<SearchableSelect
value={formData.join_table}
onValueChange={(v) =>
setFormData({ ...formData, join_table: v, join_column: "", display_column: "" })
}
options={tableOptions}
placeholder="테이블 선택"
/>
</div>
<div>
<Label className="text-xs"> *</Label>
<SearchableSelect
value={formData.join_column}
onValueChange={(v) => setFormData({ ...formData, join_column: v })}
options={targetColumnOptions}
placeholder="컬럼 선택"
disabled={!formData.join_table}
/>
</div>
<div>
<Label className="text-xs"> </Label>
<SearchableSelect
value={formData.display_column}
onValueChange={(v) => setFormData({ ...formData, display_column: v })}
options={targetColumnOptions}
placeholder="선택 (옵션)"
disabled={!formData.join_table}
/>
</div>
<div>
<Label className="text-xs"> </Label>
<SearchableSelect
value={formData.join_type}
onValueChange={(v) => setFormData({ ...formData, join_type: v })}
options={[
{ value: "LEFT", label: "LEFT JOIN" },
{ value: "INNER", label: "INNER JOIN" },
{ value: "RIGHT", label: "RIGHT JOIN" },
]}
placeholder="타입 선택"
/>
</div>
</div>
<div className="flex justify-end gap-2">
{isEditing && (
<Button variant="outline" size="sm" onClick={resetForm}>
</Button>
)}
<Button size="sm" onClick={handleSave} className="gap-1">
<Save className="h-4 w-4" />
{isEditing ? "수정" : "추가"}
</Button>
</div>
</div>
)}
{/* 목록 */}
<div className="rounded-lg border">
<Table>
<TableHeader>
<TableRow>
<TableHead className="text-xs"></TableHead>
<TableHead className="text-xs"> </TableHead>
<TableHead className="text-xs"> </TableHead>
<TableHead className="text-xs"> </TableHead>
<TableHead className="text-xs"></TableHead>
<TableHead className="w-[100px] text-xs"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{unifiedJoins.length === 0 ? (
<TableRow>
<TableCell
colSpan={6}
className="py-8 text-center text-sm text-muted-foreground"
>
.
</TableCell>
</TableRow>
) : (
unifiedJoins.map((item) => (
<TableRow
key={item.id}
className={item.source === "designer" ? "bg-blue-50" : ""}
>
<TableCell className="text-xs">
<Badge
variant="outline"
className={cn(
"text-xs",
item.source === "designer"
? "bg-blue-100 text-blue-700"
: "bg-gray-100 text-gray-700"
)}
>
{item.source === "designer" ? "화면" : "DB"}
</Badge>
</TableCell>
<TableCell className="text-xs font-medium">
{item.save_column}
</TableCell>
<TableCell className="text-xs">
{"join_table_label" in item && item.join_table_label
? item.join_table_label
: item.join_table}
</TableCell>
<TableCell className="text-xs">{item.join_column}</TableCell>
<TableCell className="text-xs">
<Badge variant="outline" className="text-xs">
{item.join_type}
</Badge>
</TableCell>
<TableCell>
<div className="flex gap-1">
{item.source === "designer" ? (
<Button
variant="outline"
size="sm"
className="h-7 text-xs"
onClick={() =>
handleSaveDesignerJoin({
column: item.save_column,
refTable: item.join_table,
refColumn: item.join_column,
})
}
disabled={!screenId}
>
<Save className="mr-1 h-3 w-3" />
DB
</Button>
) : (
<>
<Button
variant="ghost"
size="icon"
className="h-7 w-7"
onClick={() => handleEdit(item as FieldJoin)}
>
<Pencil className="h-3 w-3" />
</Button>
<Button
variant="ghost"
size="icon"
className="h-7 w-7 text-destructive"
onClick={() => handleDelete(item.id as number)}
>
<Trash2 className="h-3 w-3" />
</Button>
</>
)}
</div>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
{/* 안내 */}
<div className="text-xs text-muted-foreground">
* 화면: 화면 (DB ) | * DB: 데이터베이스에 (/ )
</div>
</div>
);
}
// ============================================================
// 탭 3: 참조 관계
// ============================================================
interface ReferenceTabProps {
tableName: string;
tableLabel?: string;
referencedBy: ReferencedBy[];
joinColumnRefs: JoinColumnRef[];
loading: boolean;
}
function ReferenceTab({
tableName,
tableLabel,
referencedBy,
joinColumnRefs,
loading,
}: ReferenceTabProps) {
if (loading) {
return (
<div className="flex h-64 items-center justify-center">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
);
}
return (
<div className="space-y-6">
{/* 이 테이블이 참조하는 테이블 */}
<div className="space-y-2">
<h3 className="flex items-center gap-2 text-sm font-semibold">
<ArrowRight className="h-4 w-4 text-orange-500" />
({joinColumnRefs.length})
</h3>
{joinColumnRefs.length > 0 ? (
<div className="space-y-2">
{joinColumnRefs.map((ref, idx) => (
<div
key={idx}
className="flex items-center gap-3 rounded-lg border bg-orange-50/50 p-3"
>
<Badge variant="outline" className="bg-orange-100 text-orange-700">
{ref.column}
</Badge>
<ArrowRight className="h-4 w-4 text-muted-foreground" />
<div className="flex-1">
<span className="font-medium">
{ref.refTableLabel || ref.refTable}
</span>
<span className="text-muted-foreground">.{ref.refColumn}</span>
</div>
</div>
))}
</div>
) : (
<div className="rounded-lg border border-dashed p-4 text-center text-sm text-muted-foreground">
.
</div>
)}
</div>
{/* 이 테이블을 참조하는 테이블 */}
<div className="space-y-2">
<h3 className="flex items-center gap-2 text-sm font-semibold">
<Eye className="h-4 w-4 text-green-500" />
({referencedBy.length})
</h3>
{referencedBy.length > 0 ? (
<div className="space-y-2">
{referencedBy.map((ref, idx) => (
<div
key={idx}
className="flex items-center gap-3 rounded-lg border bg-green-50/50 p-3"
>
<div className="flex-1">
<span className="font-medium">
{ref.fromTableLabel || ref.fromTable}
</span>
<span className="text-muted-foreground">.{ref.fromColumn}</span>
</div>
<ArrowRight className="h-4 w-4 text-muted-foreground" />
<Badge variant="outline" className="bg-green-100 text-green-700">
{ref.toColumn}
</Badge>
</div>
))}
</div>
) : (
<div className="rounded-lg border border-dashed p-4 text-center text-sm text-muted-foreground">
.
</div>
)}
</div>
</div>
);
}
export default TableSettingModal;