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

1275 lines
43 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 { Switch } from "@/components/ui/switch";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
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,
Settings,
Monitor,
ExternalLink,
Type,
Hash,
Calendar,
ToggleLeft,
FileText,
Search,
List,
X,
} from "lucide-react";
import {
getFieldJoins,
createFieldJoin,
updateFieldJoin,
deleteFieldJoin,
FieldJoin,
} from "@/lib/api/screenGroup";
import { tableManagementApi, ColumnTypeInfo, TableInfo, ColumnSettings } from "@/lib/api/tableManagement";
import { screenApi } from "@/lib/api/screen";
import { INPUT_TYPE_OPTIONS } from "@/types/input-types";
import TableManagementPage from "@/app/(main)/admin/systemMng/tableMngList/page";
// ============================================================
// 타입 정의
// ============================================================
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 ScreenUsingTable {
screenId: number;
screenName: string;
screenCode?: string;
tableRole: string; // main, filter, join
}
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>
);
}
// 입력 타입별 아이콘
function getInputTypeIcon(inputType: string) {
switch (inputType) {
case "text":
return <Type className="h-3 w-3" />;
case "number":
return <Hash className="h-3 w-3" />;
case "date":
case "datetime":
return <Calendar className="h-3 w-3" />;
case "boolean":
case "checkbox":
return <ToggleLeft className="h-3 w-3" />;
case "textarea":
return <FileText className="h-3 w-3" />;
case "entity":
return <Link2 className="h-3 w-3" />;
case "code":
return <List className="h-3 w-3" />;
default:
return <Type className="h-3 w-3" />;
}
}
// ============================================================
// 메인 모달 컴포넌트
// ============================================================
export function TableSettingModal({
isOpen,
onClose,
tableName,
tableLabel,
screenId,
joinColumnRefs = [],
referencedBy = [],
columns = [],
filterColumns = [],
onSaveSuccess,
}: TableSettingModalProps) {
const [activeTab, setActiveTab] = useState("columns");
const [loading, setLoading] = useState(false);
const [saving, setSaving] = useState(false);
const [tableColumns, setTableColumns] = useState<ColumnTypeInfo[]>([]);
const [tables, setTables] = useState<TableInfo[]>([]);
const [fieldJoins, setFieldJoins] = useState<FieldJoin[]>([]);
const [screensUsingTable, setScreensUsingTable] = useState<ScreenUsingTable[]>([]);
// 선택된 컬럼
const [selectedColumn, setSelectedColumn] = useState<string | null>(null);
// 컬럼 설정 편집 상태
const [editedColumns, setEditedColumns] = useState<Record<string, Partial<ColumnTypeInfo>>>({});
// 테이블 라벨/설명
const [editedTableLabel, setEditedTableLabel] = useState(tableLabel || "");
const [editedTableDescription, setEditedTableDescription] = useState("");
// 참조 테이블 컬럼 캐시
const [refTableColumns, setRefTableColumns] = useState<Record<string, ColumnTypeInfo[]>>({});
const [loadingRefColumns, setLoadingRefColumns] = useState(false);
// 테이블 타입 관리 모달 상태
const [showTableManagementModal, setShowTableManagementModal] = useState(false);
// 테이블 컬럼 정보 로드
const loadTableData = useCallback(async () => {
if (!tableName) return;
setLoading(true);
try {
// 테이블 목록 로드
const tablesResponse = await tableManagementApi.getTableList();
if (tablesResponse.success && tablesResponse.data) {
setTables(tablesResponse.data);
}
// 테이블 컬럼 로드 (column_labels 정보 포함)
const columnsResponse = await tableManagementApi.getColumnList(tableName);
if (columnsResponse.success && columnsResponse.data?.columns) {
// 백엔드 응답은 camelCase로 옴
const columnsData = columnsResponse.data.columns;
setTableColumns(columnsData);
// 초기 편집 상태 설정
const initialEdits: Record<string, Partial<ColumnTypeInfo>> = {};
columnsData.forEach((col) => {
initialEdits[col.columnName] = {
displayName: col.displayName,
inputType: col.inputType || "direct",
referenceTable: col.referenceTable,
referenceColumn: col.referenceColumn,
displayColumn: col.displayColumn,
};
});
setEditedColumns(initialEdits);
}
// 필드 조인 로드 (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);
}
}
// 이 테이블을 사용하는 화면 목록 로드
await loadScreensUsingTable();
} catch (error) {
console.error("테이블 정보 로드 실패:", error);
toast.error("테이블 정보를 불러오는데 실패했습니다.");
} finally {
setLoading(false);
}
}, [tableName, screenId]);
// 이 테이블을 사용하는 화면 목록 로드
const loadScreensUsingTable = useCallback(async () => {
if (!tableName) return;
try {
// 모든 화면 조회
const screensResponse = await screenApi.getScreens({ size: 1000 });
if (screensResponse.items) {
const usingScreens: ScreenUsingTable[] = [];
screensResponse.items.forEach((screen: any) => {
// 메인 테이블로 사용하는 경우
if (screen.tableName === tableName) {
usingScreens.push({
screenId: screen.screenId,
screenName: screen.screenName,
screenCode: screen.screenCode,
tableRole: "main",
});
}
// TODO: 필터 테이블, 조인 테이블로 사용하는 경우도 추가
});
setScreensUsingTable(usingScreens);
}
} catch (error) {
console.error("화면 목록 로드 실패:", error);
}
}, [tableName]);
// 참조 테이블 컬럼 로드
const loadRefTableColumns = useCallback(async (refTableName: string) => {
if (!refTableName || refTableName === "none" || refTableColumns[refTableName]) return;
setLoadingRefColumns(true);
try {
const response = await tableManagementApi.getColumnList(refTableName);
if (response.success && response.data?.columns) {
setRefTableColumns((prev) => ({
...prev,
[refTableName]: response.data!.columns,
}));
}
} catch (error) {
console.error("참조 테이블 컬럼 로드 실패:", error);
} finally {
setLoadingRefColumns(false);
}
}, [refTableColumns]);
useEffect(() => {
if (isOpen && tableName) {
loadTableData();
setEditedTableLabel(tableLabel || tableName);
}
}, [isOpen, tableName, tableLabel, loadTableData]);
// 참조 테이블 변경 시 컬럼 로드
useEffect(() => {
Object.values(editedColumns).forEach((col) => {
if (col.referenceTable && col.referenceTable !== "none") {
loadRefTableColumns(col.referenceTable);
}
});
}, [editedColumns, loadRefTableColumns]);
// 새로고침
const handleRefresh = () => {
loadTableData();
toast.success("새로고침 완료");
};
// 컬럼 설정 변경 핸들러
const handleColumnChange = (columnName: string, field: string, value: any) => {
setEditedColumns((prev) => ({
...prev,
[columnName]: {
...prev[columnName],
[field]: value,
},
}));
// 참조 테이블 변경 시 참조 컬럼 초기화
if (field === "referenceTable") {
setEditedColumns((prev) => ({
...prev,
[columnName]: {
...prev[columnName],
referenceColumn: "",
displayColumn: "",
},
}));
if (value && value !== "none") {
loadRefTableColumns(value);
}
}
};
// 전체 저장 (테이블타입관리 페이지와 동일한 로직)
const handleSaveAll = async () => {
setSaving(true);
try {
// 변경된 컬럼들만 저장
for (const [columnName, editedSettings] of Object.entries(editedColumns)) {
// 기존 컬럼 정보 찾기
const originalColumn = tableColumns.find((c) => c.columnName === columnName);
if (!originalColumn) continue;
// 기존 값과 편집된 값 병합
const mergedColumn = {
...originalColumn,
...editedSettings,
};
// detailSettings 처리 (Entity 타입인 경우)
let finalDetailSettings = mergedColumn.detailSettings || "";
if (mergedColumn.inputType === "entity" && mergedColumn.referenceTable) {
// 기존 detailSettings를 파싱하거나 새로 생성
let existingSettings: Record<string, unknown> = {};
if (typeof mergedColumn.detailSettings === "string" && mergedColumn.detailSettings.trim().startsWith("{")) {
try {
existingSettings = JSON.parse(mergedColumn.detailSettings);
} catch {
existingSettings = {};
}
}
// 엔티티 설정 추가
const entitySettings = {
...existingSettings,
entityTable: mergedColumn.referenceTable,
entityCodeColumn: mergedColumn.referenceColumn || "id",
entityLabelColumn: mergedColumn.displayColumn || "name",
placeholder: (existingSettings.placeholder as string) || "항목을 선택하세요",
searchable: existingSettings.searchable ?? true,
};
finalDetailSettings = JSON.stringify(entitySettings);
console.log("Entity 설정 JSON 생성:", entitySettings);
}
// Code 타입인 경우 hierarchyRole을 detailSettings에 포함
if (mergedColumn.inputType === "code" && (mergedColumn as any).hierarchyRole) {
let existingSettings: Record<string, unknown> = {};
if (typeof finalDetailSettings === "string" && finalDetailSettings.trim().startsWith("{")) {
try {
existingSettings = JSON.parse(finalDetailSettings);
} catch {
existingSettings = {};
}
}
const codeSettings = {
...existingSettings,
hierarchyRole: (mergedColumn as any).hierarchyRole,
};
finalDetailSettings = JSON.stringify(codeSettings);
console.log("Code 계층 역할 설정 JSON 생성:", codeSettings);
}
// ColumnSettings 인터페이스에 맞게 데이터 구성
const columnSetting: ColumnSettings = {
columnName: columnName,
columnLabel: mergedColumn.displayName || originalColumn.displayName || "",
webType: mergedColumn.inputType || originalColumn.inputType || "text",
detailSettings: finalDetailSettings,
codeCategory: mergedColumn.codeCategory || originalColumn.codeCategory || "",
codeValue: mergedColumn.codeValue || originalColumn.codeValue || "",
referenceTable: mergedColumn.referenceTable || "",
referenceColumn: mergedColumn.referenceColumn || "",
displayColumn: mergedColumn.displayColumn || "",
};
console.log("저장할 컬럼 설정:", columnSetting);
// API 호출
const response = await tableManagementApi.updateColumnSettings(tableName, columnName, columnSetting);
if (!response.success) {
console.error(`컬럼 '${columnName}' 저장 실패:`, response.message);
toast.error(`컬럼 '${columnName}' 저장 실패: ${response.message}`);
return;
}
}
toast.success("테이블 설정이 저장되었습니다.");
setEditedColumns({}); // 편집 상태 초기화
onSaveSuccess?.();
await loadTableData();
} catch (error) {
console.error("저장 실패:", error);
toast.error("저장에 실패했습니다.");
} finally {
setSaving(false);
}
};
// 컬럼 정보 통합
const mergedColumns = useMemo(() => {
const columnsMap = new Map<string, ColumnTypeInfo & { isPK?: boolean; isFK?: boolean }>();
// API에서 가져온 컬럼 정보 (camelCase)
tableColumns.forEach((tcol) => {
columnsMap.set(tcol.columnName, {
...tcol,
isPK: tcol.isPrimaryKey,
isFK: false, // 백엔드에서 isForeignKey를 제공하지 않으므로 false 기본값
});
});
return Array.from(columnsMap.values());
}, [tableColumns]);
// 선택된 컬럼 정보
const selectedColumnInfo = useMemo(() => {
if (!selectedColumn) return null;
return mergedColumns.find((c) => c.columnName === selectedColumn);
}, [selectedColumn, mergedColumns]);
// 테이블 옵션
const tableOptions = useMemo(
() => [
{ value: "none", label: "-- 선택 안함 --" },
...tables.map((t) => ({
value: t.tableName,
label: t.displayName || t.tableName,
description: t.tableName,
})),
],
[tables]
);
// 입력 타입 옵션
const inputTypeOptions = useMemo(
() =>
INPUT_TYPE_OPTIONS.map((opt) => ({
value: opt.value,
label: opt.label,
})),
[]
);
// 참조 테이블 컬럼 옵션
const getRefColumnOptions = (refTable: string) => {
const cols = refTableColumns[refTable] || [];
return [
{ value: "", label: "-- 선택 안함 --" },
...cols.map((c) => ({
value: c.columnName,
label: c.displayName || c.columnName,
description: c.dataType,
})),
];
};
return (
<>
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="flex h-[85vh] max-h-[900px] w-[95vw] max-w-[1200px] flex-col">
<DialogHeader className="flex-shrink-0">
<div className="flex items-center justify-between">
<DialogTitle className="flex items-center gap-2 text-lg">
<Table2 className="h-5 w-5 text-green-500" />
: {tableLabel || tableName}
{tableName !== tableLabel && tableName !== (tableLabel || tableName) && (
<span className="text-sm font-normal text-muted-foreground">
({tableName})
</span>
)}
</DialogTitle>
<Button
variant="outline"
size="sm"
onClick={() => setShowTableManagementModal(true)}
className="gap-1 text-xs"
>
<Settings className="h-3.5 w-3.5" />
</Button>
</div>
<DialogDescription className="text-sm">
, , .
</DialogDescription>
</DialogHeader>
<div className="flex min-h-0 flex-1 gap-4">
{/* 좌측: 탭 (40%) */}
<div className="flex w-[40%] min-h-0 flex-col">
<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="columns" className="gap-1 text-xs">
<Columns3 className="h-3.5 w-3.5" />
</TabsTrigger>
<TabsTrigger value="screens" className="gap-1 text-xs">
<Monitor 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>
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={handleRefresh}
className="h-8 gap-1"
disabled={loading}
>
<RefreshCw className={cn("h-3.5 w-3.5", loading && "animate-spin")} />
</Button>
<Button
size="sm"
onClick={handleSaveAll}
className="h-8 gap-1"
disabled={saving || loading}
>
{saving ? (
<Loader2 className="h-3.5 w-3.5 animate-spin" />
) : (
<Save className="h-3.5 w-3.5" />
)}
</Button>
</div>
</div>
{/* 탭 1: 컬럼 설정 */}
<TabsContent value="columns" className="mt-0 min-h-0 flex-1 overflow-auto">
<ColumnListTab
columns={mergedColumns}
editedColumns={editedColumns}
selectedColumn={selectedColumn}
onSelectColumn={setSelectedColumn}
loading={loading}
/>
</TabsContent>
{/* 탭 2: 화면 연동 */}
<TabsContent value="screens" className="mt-0 min-h-0 flex-1 overflow-auto p-3">
<ScreensTab
screensUsingTable={screensUsingTable}
loading={loading}
/>
</TabsContent>
{/* 탭 3: 참조 관계 */}
<TabsContent value="references" className="mt-0 min-h-0 flex-1 overflow-auto p-3">
<ReferenceTab
tableName={tableName}
tableLabel={tableLabel}
referencedBy={referencedBy}
joinColumnRefs={joinColumnRefs}
loading={loading}
/>
</TabsContent>
</Tabs>
</div>
{/* 우측: 컬럼 상세 설정 (60%) */}
<div className="flex w-[60%] min-h-0 flex-col rounded-lg border bg-muted/30 p-4">
{selectedColumn && selectedColumnInfo ? (
<ColumnDetailPanel
columnInfo={selectedColumnInfo}
editedColumn={editedColumns[selectedColumn] || {}}
tableOptions={tableOptions}
inputTypeOptions={inputTypeOptions}
getRefColumnOptions={getRefColumnOptions}
loadingRefColumns={loadingRefColumns}
onColumnChange={(field, value) => handleColumnChange(selectedColumn, field, value)}
/>
) : (
<div className="flex h-full items-center justify-center text-sm text-muted-foreground">
<div className="text-center">
<Columns3 className="mx-auto h-12 w-12 text-muted-foreground/30" />
<p className="mt-2"> </p>
<p> .</p>
</div>
</div>
)}
</div>
</div>
</DialogContent>
</Dialog>
{/* 테이블 타입 관리 전체 화면 모달 */}
<Dialog open={showTableManagementModal} onOpenChange={setShowTableManagementModal}>
<DialogContent className="max-w-[98vw] h-[95vh] p-0 overflow-hidden">
<div className="flex flex-col h-full">
{/* 헤더 */}
<div className="flex items-center justify-between px-4 py-3 border-b bg-background">
<div>
<DialogTitle className="text-lg font-semibold">
</DialogTitle>
<p className="text-xs text-muted-foreground">
</p>
</div>
<Button
variant="ghost"
size="sm"
onClick={() => {
setShowTableManagementModal(false);
// 테이블 데이터 새로고침
loadTableData();
}}
>
<X className="h-4 w-4" />
</Button>
</div>
{/* TableManagementPage */}
<div className="flex-1 overflow-hidden">
<TableManagementPage />
</div>
</div>
</DialogContent>
</Dialog>
</>
);
}
// ============================================================
// 탭 1: 컬럼 목록
// ============================================================
interface ColumnListTabProps {
columns: (ColumnTypeInfo & { isPK?: boolean; isFK?: boolean })[];
editedColumns: Record<string, Partial<ColumnTypeInfo>>;
selectedColumn: string | null;
onSelectColumn: (columnName: string) => void;
loading: boolean;
}
function ColumnListTab({
columns,
editedColumns,
selectedColumn,
onSelectColumn,
loading,
}: ColumnListTabProps) {
const [searchTerm, setSearchTerm] = useState("");
const filteredColumns = useMemo(() => {
if (!searchTerm) return columns;
const term = searchTerm.toLowerCase();
return columns.filter(
(col) =>
col.columnName.toLowerCase().includes(term) ||
(col.displayName || "").toLowerCase().includes(term)
);
}, [columns, searchTerm]);
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="flex h-full flex-col">
{/* 검색 */}
<div className="flex-shrink-0 p-3 pb-2">
<div className="relative">
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input
placeholder="컬럼 검색..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="h-9 pl-9 text-sm"
/>
</div>
</div>
{/* 통계 */}
<div className="flex-shrink-0 px-3 pb-2">
<div className="flex gap-2 text-xs text-muted-foreground">
<span> {columns.length}</span>
<span></span>
<span>PK {columns.filter((c) => c.isPK).length}</span>
<span></span>
<span>FK {columns.filter((c) => c.isFK).length}</span>
</div>
</div>
{/* 컬럼 목록 */}
<div className="flex-1 overflow-y-auto">
{filteredColumns.length === 0 ? (
<div className="py-8 text-center text-sm text-muted-foreground">
{searchTerm ? "검색 결과가 없습니다." : "컬럼이 없습니다."}
</div>
) : (
<div className="space-y-1 px-3 pb-3">
{filteredColumns.map((col) => {
const edited = editedColumns[col.columnName] || {};
const inputType = (edited.inputType || col.inputType || "text") as string;
const isSelected = selectedColumn === col.columnName;
return (
<div
key={col.columnName}
onClick={() => onSelectColumn(col.columnName)}
className={cn(
"cursor-pointer rounded-lg border p-3 transition-colors",
isSelected
? "border-green-300 bg-green-50"
: "border-transparent bg-background hover:bg-muted/50"
)}
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="text-xs text-muted-foreground">
{getInputTypeIcon(inputType)}
</span>
<span className="text-sm font-medium">
{edited.displayName || col.displayName || col.columnName}
</span>
</div>
<div className="flex gap-1">
{col.isPK && (
<Badge variant="outline" className="bg-orange-100 text-orange-700 text-[10px] px-1.5">
<Key className="mr-0.5 h-2.5 w-2.5" />
PK
</Badge>
)}
{col.isFK && (
<Badge variant="outline" className="bg-green-100 text-green-700 text-[10px] px-1.5">
<Link2 className="mr-0.5 h-2.5 w-2.5" />
FK
</Badge>
)}
{(edited.referenceTable || col.referenceTable) && (
<Badge variant="outline" className="bg-blue-100 text-blue-700 text-[10px] px-1.5">
</Badge>
)}
</div>
</div>
<div className="mt-1 flex items-center gap-2 text-xs text-muted-foreground">
<span className="font-mono">{col.columnName}</span>
<span></span>
<span>{col.dataType}</span>
</div>
</div>
);
})}
</div>
)}
</div>
</div>
);
}
// ============================================================
// 컬럼 상세 설정 패널
// ============================================================
interface ColumnDetailPanelProps {
columnInfo: ColumnTypeInfo & { isPK?: boolean; isFK?: boolean };
editedColumn: Partial<ColumnTypeInfo>;
tableOptions: Array<{ value: string; label: string; description?: string }>;
inputTypeOptions: Array<{ value: string; label: string }>;
getRefColumnOptions: (refTable: string) => Array<{ value: string; label: string; description?: string }>;
loadingRefColumns: boolean;
onColumnChange: (field: string, value: any) => void;
}
function ColumnDetailPanel({
columnInfo,
editedColumn,
tableOptions,
inputTypeOptions,
getRefColumnOptions,
loadingRefColumns,
onColumnChange,
}: ColumnDetailPanelProps) {
const currentLabel = editedColumn.displayName ?? columnInfo.displayName ?? "";
const currentInputType = (editedColumn.inputType ?? columnInfo.inputType ?? "text") as string;
const currentRefTable = editedColumn.referenceTable ?? columnInfo.referenceTable ?? "";
const currentRefColumn = editedColumn.referenceColumn ?? columnInfo.referenceColumn ?? "";
const currentDisplayColumn = editedColumn.displayColumn ?? columnInfo.displayColumn ?? "";
return (
<div className="flex h-full flex-col">
{/* 헤더 */}
<div className="flex-shrink-0 border-b pb-4">
<div className="flex items-center gap-2">
<Settings2 className="h-5 w-5 text-green-500" />
<h3 className="text-lg font-semibold"> </h3>
</div>
<div className="mt-2 flex items-center gap-2">
<span className="font-mono text-sm bg-muted px-2 py-0.5 rounded">
{columnInfo.columnName}
</span>
<span className="text-xs text-muted-foreground">{columnInfo.dataType}</span>
{columnInfo.isPK && (
<Badge variant="outline" className="bg-orange-100 text-orange-700 text-[10px]">
Primary Key
</Badge>
)}
{columnInfo.isFK && (
<Badge variant="outline" className="bg-green-100 text-green-700 text-[10px]">
Foreign Key
</Badge>
)}
</div>
</div>
{/* 설정 폼 */}
<div className="flex-1 overflow-y-auto py-4 space-y-6">
{/* 기본 정보 */}
<div className="space-y-4">
<h4 className="text-sm font-semibold text-muted-foreground"> </h4>
<div className="space-y-2">
<Label className="text-xs"> </Label>
<Input
value={currentLabel}
onChange={(e) => onColumnChange("displayName", e.target.value)}
placeholder={columnInfo.columnName}
className="h-9 text-sm"
/>
<p className="text-[10px] text-muted-foreground">
.
</p>
</div>
<div className="space-y-2">
<Label className="text-xs"> </Label>
<Select
value={currentInputType}
onValueChange={(v) => onColumnChange("inputType", v)}
>
<SelectTrigger className="h-9 text-sm">
<SelectValue placeholder="입력 타입 선택" />
</SelectTrigger>
<SelectContent>
{inputTypeOptions.map((opt) => (
<SelectItem key={opt.value} value={opt.value}>
<div className="flex items-center gap-2">
{getInputTypeIcon(opt.value)}
{opt.label}
</div>
</SelectItem>
))}
</SelectContent>
</Select>
<p className="text-[10px] text-muted-foreground">
.
</p>
</div>
</div>
{/* 조인 설정 (Entity 타입일 때만) */}
{currentInputType === "entity" && (
<div className="space-y-4">
<h4 className="text-sm font-semibold text-muted-foreground">Entity </h4>
<div className="space-y-2">
<Label className="text-xs"> </Label>
<SearchableSelect
value={currentRefTable || "none"}
onValueChange={(v) => onColumnChange("referenceTable", v === "none" ? "" : v)}
options={tableOptions}
placeholder="테이블 선택"
/>
</div>
{currentRefTable && currentRefTable !== "none" && (
<>
<div className="space-y-2">
<Label className="text-xs"> (PK)</Label>
{loadingRefColumns ? (
<div className="flex items-center gap-2 h-9 text-sm text-muted-foreground">
<Loader2 className="h-4 w-4 animate-spin" />
...
</div>
) : (
<SearchableSelect
value={currentRefColumn}
onValueChange={(v) => onColumnChange("referenceColumn", v)}
options={getRefColumnOptions(currentRefTable)}
placeholder="컬럼 선택"
/>
)}
<p className="text-[10px] text-muted-foreground">
.
</p>
</div>
<div className="space-y-2">
<Label className="text-xs"> </Label>
{loadingRefColumns ? (
<div className="flex items-center gap-2 h-9 text-sm text-muted-foreground">
<Loader2 className="h-4 w-4 animate-spin" />
...
</div>
) : (
<SearchableSelect
value={currentDisplayColumn}
onValueChange={(v) => onColumnChange("displayColumn", v)}
options={getRefColumnOptions(currentRefTable)}
placeholder="표시 컬럼 선택"
/>
)}
<p className="text-[10px] text-muted-foreground">
.
</p>
</div>
</>
)}
</div>
)}
{/* 컬럼 정보 (읽기 전용) */}
<div className="space-y-4">
<h4 className="text-sm font-semibold text-muted-foreground"> </h4>
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<span className="text-xs text-muted-foreground"> </span>
<p className="font-mono">{columnInfo.dataType}</p>
</div>
<div>
<span className="text-xs text-muted-foreground">NULL </span>
<p>{columnInfo.isNullable === "YES" ? "예" : "아니오"}</p>
</div>
{columnInfo.maxLength && (
<div>
<span className="text-xs text-muted-foreground"> </span>
<p>{columnInfo.maxLength}</p>
</div>
)}
{columnInfo.defaultValue && (
<div>
<span className="text-xs text-muted-foreground"></span>
<p className="font-mono text-xs">{columnInfo.defaultValue}</p>
</div>
)}
</div>
</div>
</div>
</div>
);
}
// ============================================================
// 탭 2: 화면 연동 현황
// ============================================================
interface ScreensTabProps {
screensUsingTable: ScreenUsingTable[];
loading: boolean;
}
function ScreensTab({ screensUsingTable, loading }: ScreensTabProps) {
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">
<div className="flex items-center justify-between">
<h3 className="text-sm font-semibold">
({screensUsingTable.length})
</h3>
</div>
{screensUsingTable.length === 0 ? (
<div className="rounded-lg border border-dashed p-8 text-center text-sm text-muted-foreground">
<Monitor className="mx-auto h-12 w-12 text-muted-foreground/30" />
<p className="mt-2"> .</p>
</div>
) : (
<div className="space-y-2">
{screensUsingTable.map((screen) => (
<div
key={screen.screenId}
className="flex items-center justify-between rounded-lg border bg-background p-3 hover:bg-muted/50"
>
<div className="flex items-center gap-3">
<Monitor className="h-4 w-4 text-blue-500" />
<div>
<p className="text-sm font-medium">{screen.screenName}</p>
{screen.screenCode && (
<p className="text-xs text-muted-foreground font-mono">
{screen.screenCode}
</p>
)}
</div>
</div>
<div className="flex items-center gap-2">
<Badge
variant="outline"
className={cn(
"text-[10px]",
screen.tableRole === "main"
? "bg-blue-100 text-blue-700"
: screen.tableRole === "filter"
? "bg-purple-100 text-purple-700"
: "bg-orange-100 text-orange-700"
)}
>
{screen.tableRole === "main"
? "메인 테이블"
: screen.tableRole === "filter"
? "필터 테이블"
: "조인 테이블"}
</Badge>
<Button variant="ghost" size="icon" className="h-7 w-7">
<ExternalLink className="h-3.5 w-3.5" />
</Button>
</div>
</div>
))}
</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;