jskim-node #419
|
|
@ -12,7 +12,7 @@ import {
|
|||
Search,
|
||||
Database,
|
||||
RefreshCw,
|
||||
Settings,
|
||||
Save,
|
||||
Plus,
|
||||
Activity,
|
||||
Trash2,
|
||||
|
|
@ -21,7 +21,6 @@ import {
|
|||
ChevronsUpDown,
|
||||
Loader2,
|
||||
} from "lucide-react";
|
||||
import { ResponsiveSplitPanel } from "@/components/common/ResponsiveSplitPanel";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { LoadingSpinner } from "@/components/common/LoadingSpinner";
|
||||
import { toast } from "sonner";
|
||||
|
|
@ -969,16 +968,24 @@ export default function TableManagementPage() {
|
|||
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||
}, [selectedTable, columns.length]);
|
||||
|
||||
// 필터링된 테이블 목록 (메모이제이션)
|
||||
const filteredTables = useMemo(
|
||||
() =>
|
||||
tables.filter(
|
||||
(table) =>
|
||||
table.tableName.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
table.displayName.toLowerCase().includes(searchTerm.toLowerCase()),
|
||||
),
|
||||
[tables, searchTerm],
|
||||
);
|
||||
// 필터링 + 한글 우선 정렬 (ㄱ~ㅎ → a~z)
|
||||
const filteredTables = useMemo(() => {
|
||||
const filtered = tables.filter(
|
||||
(table) =>
|
||||
table.tableName.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
table.displayName.toLowerCase().includes(searchTerm.toLowerCase()),
|
||||
);
|
||||
const isKorean = (str: string) => /^[가-힣ㄱ-ㅎ]/.test(str);
|
||||
return filtered.sort((a, b) => {
|
||||
const nameA = a.displayName || a.tableName;
|
||||
const nameB = b.displayName || b.tableName;
|
||||
const aKo = isKorean(nameA);
|
||||
const bKo = isKorean(nameB);
|
||||
if (aKo && !bKo) return -1;
|
||||
if (!aKo && bKo) return 1;
|
||||
return nameA.localeCompare(nameB, aKo ? "ko" : "en");
|
||||
});
|
||||
}, [tables, searchTerm]);
|
||||
|
||||
// 선택된 테이블 정보
|
||||
const selectedTableInfo = tables.find((table) => table.tableName === selectedTable);
|
||||
|
|
@ -1292,339 +1299,338 @@ export default function TableManagementPage() {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="bg-background flex h-screen flex-col">
|
||||
<div className="flex h-full flex-col space-y-6 overflow-hidden p-6">
|
||||
{/* 페이지 헤더 */}
|
||||
<div className="flex-shrink-0 space-y-2 border-b pb-4">
|
||||
<div className="flex flex-col gap-2 lg:flex-row lg:items-start lg:justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight">
|
||||
{getTextFromUI(TABLE_MANAGEMENT_KEYS.PAGE_TITLE, "테이블 타입 관리")}
|
||||
</h1>
|
||||
<p className="text-muted-foreground mt-2 text-sm">
|
||||
{getTextFromUI(
|
||||
TABLE_MANAGEMENT_KEYS.PAGE_DESCRIPTION,
|
||||
"데이터베이스 테이블과 컬럼의 타입을 관리합니다",
|
||||
)}
|
||||
</p>
|
||||
{isSuperAdmin && (
|
||||
<p className="text-primary mt-1 text-sm font-medium">
|
||||
최고 관리자 권한으로 새 테이블 생성 및 컬럼 추가가 가능합니다
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{/* DDL 기능 버튼들 (최고 관리자만) */}
|
||||
{isSuperAdmin && (
|
||||
<>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setDuplicateModalMode("create");
|
||||
setDuplicateSourceTable(null);
|
||||
setCreateTableModalOpen(true);
|
||||
}}
|
||||
className="h-10 gap-2 text-sm font-medium"
|
||||
size="default"
|
||||
>
|
||||
<Plus className="h-4 w-4" />새 테이블 생성
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (selectedTableIds.size !== 1) {
|
||||
toast.error("복제할 테이블을 1개만 선택해주세요.");
|
||||
return;
|
||||
}
|
||||
const sourceTable = Array.from(selectedTableIds)[0];
|
||||
setDuplicateSourceTable(sourceTable);
|
||||
setDuplicateModalMode("duplicate");
|
||||
setCreateTableModalOpen(true);
|
||||
}}
|
||||
variant="outline"
|
||||
disabled={selectedTableIds.size !== 1}
|
||||
className="h-10 gap-2 text-sm font-medium"
|
||||
>
|
||||
<Copy className="h-4 w-4" />
|
||||
테이블 복제
|
||||
</Button>
|
||||
|
||||
{selectedTable && (
|
||||
<Button
|
||||
onClick={() => setAddColumnModalOpen(true)}
|
||||
variant="outline"
|
||||
className="h-10 gap-2 text-sm font-medium"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
컬럼 추가
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button
|
||||
onClick={() => setDdlLogViewerOpen(true)}
|
||||
variant="outline"
|
||||
className="h-10 gap-2 text-sm font-medium"
|
||||
>
|
||||
<Activity className="h-4 w-4" />
|
||||
DDL 로그
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="bg-background flex h-screen flex-col overflow-hidden">
|
||||
{/* 컴팩트 탑바 (52px) */}
|
||||
<div className="flex h-[52px] flex-shrink-0 items-center justify-between border-b px-5">
|
||||
<div className="flex items-center gap-3">
|
||||
<Database className="h-4.5 w-4.5 text-muted-foreground" />
|
||||
<h1 className="text-[15px] font-bold tracking-tight">
|
||||
{getTextFromUI(TABLE_MANAGEMENT_KEYS.PAGE_TITLE, "테이블 타입 관리")}
|
||||
</h1>
|
||||
<Badge variant="secondary" className="text-[10px] font-bold">
|
||||
{tables.length} 테이블
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
{isSuperAdmin && (
|
||||
<>
|
||||
<Button
|
||||
onClick={loadTables}
|
||||
disabled={loading}
|
||||
variant="outline"
|
||||
className="h-10 gap-2 text-sm font-medium"
|
||||
onClick={() => {
|
||||
setDuplicateModalMode("create");
|
||||
setDuplicateSourceTable(null);
|
||||
setCreateTableModalOpen(true);
|
||||
}}
|
||||
size="sm"
|
||||
className="h-8 gap-1.5 text-xs"
|
||||
>
|
||||
<RefreshCw className={`h-4 w-4 ${loading ? "animate-spin" : ""}`} />
|
||||
{getTextFromUI(TABLE_MANAGEMENT_KEYS.BUTTON_REFRESH, "새로고침")}
|
||||
<Plus className="h-3.5 w-3.5" />
|
||||
새 테이블
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (selectedTableIds.size !== 1) {
|
||||
toast.error("복제할 테이블을 1개만 선택해주세요.");
|
||||
return;
|
||||
}
|
||||
const sourceTable = Array.from(selectedTableIds)[0];
|
||||
setDuplicateSourceTable(sourceTable);
|
||||
setDuplicateModalMode("duplicate");
|
||||
setCreateTableModalOpen(true);
|
||||
}}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={selectedTableIds.size !== 1}
|
||||
className="h-8 gap-1.5 text-xs"
|
||||
>
|
||||
<Copy className="h-3.5 w-3.5" />
|
||||
복제
|
||||
</Button>
|
||||
{selectedTable && (
|
||||
<Button
|
||||
onClick={() => setAddColumnModalOpen(true)}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-8 gap-1.5 text-xs"
|
||||
>
|
||||
<Plus className="h-3.5 w-3.5" />
|
||||
컬럼 추가
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
onClick={() => setDdlLogViewerOpen(true)}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-8 gap-1.5 text-xs"
|
||||
>
|
||||
<Activity className="h-3.5 w-3.5" />
|
||||
DDL
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
<Button
|
||||
onClick={loadTables}
|
||||
disabled={loading}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-8 gap-1.5 text-xs"
|
||||
>
|
||||
<RefreshCw className={cn("h-3.5 w-3.5", loading && "animate-spin")} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 3패널 메인 */}
|
||||
<div className="flex flex-1 overflow-hidden">
|
||||
{/* 좌측: 테이블 목록 (240px) */}
|
||||
<div className="bg-card flex w-[240px] min-w-[240px] flex-shrink-0 flex-col border-r">
|
||||
{/* 검색 */}
|
||||
<div className="flex-shrink-0 p-3 pb-0">
|
||||
<div className="relative">
|
||||
<Search className="text-muted-foreground absolute top-1/2 left-2.5 h-3.5 w-3.5 -translate-y-1/2" />
|
||||
<Input
|
||||
placeholder={getTextFromUI(TABLE_MANAGEMENT_KEYS.SEARCH_PLACEHOLDER, "테이블 검색...")}
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="bg-background h-[34px] pl-8 text-xs"
|
||||
/>
|
||||
</div>
|
||||
{isSuperAdmin && (
|
||||
<div className="mt-2 flex items-center justify-between border-b pb-2">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Checkbox
|
||||
checked={
|
||||
filteredTables.length > 0 &&
|
||||
filteredTables.every((table) => selectedTableIds.has(table.tableName))
|
||||
}
|
||||
onCheckedChange={handleSelectAll}
|
||||
aria-label="전체 선택"
|
||||
className="h-3.5 w-3.5"
|
||||
/>
|
||||
<span className="text-muted-foreground text-[10px]">
|
||||
{selectedTableIds.size > 0 ? `${selectedTableIds.size}개` : "전체"}
|
||||
</span>
|
||||
</div>
|
||||
{selectedTableIds.size > 0 && (
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
onClick={handleBulkDeleteClick}
|
||||
className="h-6 gap-1 px-2 text-[10px]"
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
삭제
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 테이블 리스트 */}
|
||||
<div className="flex-1 overflow-y-auto px-1">
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
) : filteredTables.length === 0 ? (
|
||||
<div className="text-muted-foreground py-8 text-center text-xs">
|
||||
{getTextFromUI(TABLE_MANAGEMENT_KEYS.MESSAGE_NO_TABLES, "테이블이 없습니다")}
|
||||
</div>
|
||||
) : (
|
||||
filteredTables.map((table, idx) => {
|
||||
const isActive = selectedTable === table.tableName;
|
||||
const prevTable = idx > 0 ? filteredTables[idx - 1] : null;
|
||||
const isKo = /^[가-힣ㄱ-ㅎ]/.test(table.displayName || table.tableName);
|
||||
const prevIsKo = prevTable ? /^[가-힣ㄱ-ㅎ]/.test(prevTable.displayName || prevTable.tableName) : null;
|
||||
const showDivider = idx === 0 || (prevIsKo !== null && isKo !== prevIsKo);
|
||||
|
||||
return (
|
||||
<div key={table.tableName}>
|
||||
{showDivider && (
|
||||
<div className="text-muted-foreground/60 mt-2 mb-1 px-2 text-[9px] font-bold uppercase tracking-widest">
|
||||
{isKo ? "한글" : "ENGLISH"}
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
"group relative flex items-center gap-2 rounded-md px-2.5 py-[7px] transition-colors",
|
||||
isActive
|
||||
? "bg-accent text-foreground"
|
||||
: "text-foreground/80 hover:bg-accent/50",
|
||||
)}
|
||||
onClick={() => handleTableSelect(table.tableName)}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault();
|
||||
handleTableSelect(table.tableName);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{isActive && (
|
||||
<div className="bg-primary absolute top-1.5 bottom-1.5 left-0 w-[3px] rounded-r" />
|
||||
)}
|
||||
{isSuperAdmin && (
|
||||
<Checkbox
|
||||
checked={selectedTableIds.has(table.tableName)}
|
||||
onCheckedChange={(checked) => handleTableCheck(table.tableName, checked as boolean)}
|
||||
aria-label={`${table.displayName || table.tableName} 선택`}
|
||||
className="h-3.5 w-3.5 flex-shrink-0"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
)}
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-baseline gap-1">
|
||||
<span className={cn(
|
||||
"truncate text-[12px] leading-tight",
|
||||
isActive ? "font-bold" : "font-medium",
|
||||
)}>
|
||||
{table.displayName || table.tableName}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-muted-foreground truncate font-mono text-[10px] leading-tight tracking-tight">
|
||||
{table.tableName}
|
||||
</div>
|
||||
</div>
|
||||
<span className={cn(
|
||||
"flex-shrink-0 rounded-full px-1.5 py-0.5 font-mono text-[10px] font-bold leading-none",
|
||||
isActive
|
||||
? "bg-primary/15 text-primary"
|
||||
: "text-muted-foreground",
|
||||
)}>
|
||||
{table.columnCount}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 하단 정보 */}
|
||||
<div className="text-muted-foreground flex-shrink-0 border-t px-3 py-2 text-[10px] font-medium">
|
||||
{filteredTables.length} / {tables.length} 테이블
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ResponsiveSplitPanel
|
||||
left={
|
||||
<div className="flex h-full flex-col space-y-4">
|
||||
{/* 검색 */}
|
||||
<div className="flex-shrink-0">
|
||||
<div className="relative">
|
||||
<Search className="text-muted-foreground absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2" />
|
||||
{/* 중앙: 컬럼 그리드 */}
|
||||
<div className="flex min-w-0 flex-1 flex-col overflow-hidden">
|
||||
{!selectedTable ? (
|
||||
<div className="flex flex-1 flex-col items-center justify-center gap-2">
|
||||
<Database className="text-muted-foreground/40 h-10 w-10" />
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{getTextFromUI(TABLE_MANAGEMENT_KEYS.SELECT_TABLE_PLACEHOLDER, "테이블을 선택해주세요")}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* 중앙 헤더: 테이블명 + 라벨 입력 + 저장 */}
|
||||
<div className="bg-card flex flex-shrink-0 items-center gap-3 border-b px-5 py-3">
|
||||
<div className="min-w-0 flex-shrink-0">
|
||||
<div className="text-[15px] font-bold tracking-tight">
|
||||
{tableLabel || selectedTable}
|
||||
</div>
|
||||
<div className="text-muted-foreground font-mono text-[11px] tracking-tight">
|
||||
{selectedTable}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex min-w-0 flex-1 items-center gap-2">
|
||||
<Input
|
||||
placeholder={getTextFromUI(TABLE_MANAGEMENT_KEYS.SEARCH_PLACEHOLDER, "테이블 검색...")}
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="h-10 pl-10 text-sm"
|
||||
value={tableLabel}
|
||||
onChange={(e) => setTableLabel(e.target.value)}
|
||||
placeholder="표시명"
|
||||
className="h-8 max-w-[160px] text-xs"
|
||||
/>
|
||||
<Input
|
||||
value={tableDescription}
|
||||
onChange={(e) => setTableDescription(e.target.value)}
|
||||
placeholder="설명"
|
||||
className="h-8 max-w-[200px] text-xs"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
onClick={saveAllSettings}
|
||||
disabled={!selectedTable || columns.length === 0 || isSaving}
|
||||
size="sm"
|
||||
className="h-8 gap-1.5 text-xs"
|
||||
>
|
||||
{isSaving ? (
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||
) : (
|
||||
<Save className="h-3.5 w-3.5" />
|
||||
)}
|
||||
{isSaving ? "저장 중..." : "전체 설정 저장"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 테이블 목록 */}
|
||||
<div className="flex-1 space-y-3 overflow-y-auto">
|
||||
{/* 전체 선택 및 일괄 삭제 (최고 관리자만) */}
|
||||
{isSuperAdmin && (
|
||||
<div className="flex items-center justify-between border-b pb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
checked={
|
||||
tables.filter(
|
||||
(table) =>
|
||||
table.tableName.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
(table.displayName && table.displayName.toLowerCase().includes(searchTerm.toLowerCase())),
|
||||
).length > 0 &&
|
||||
tables
|
||||
.filter(
|
||||
(table) =>
|
||||
table.tableName.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
(table.displayName &&
|
||||
table.displayName.toLowerCase().includes(searchTerm.toLowerCase())),
|
||||
)
|
||||
.every((table) => selectedTableIds.has(table.tableName))
|
||||
}
|
||||
onCheckedChange={handleSelectAll}
|
||||
aria-label="전체 선택"
|
||||
/>
|
||||
<span className="text-muted-foreground text-sm">
|
||||
{selectedTableIds.size > 0 && `${selectedTableIds.size}개 선택됨`}
|
||||
</span>
|
||||
</div>
|
||||
{selectedTableIds.size > 0 && (
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
onClick={handleBulkDeleteClick}
|
||||
className="h-8 gap-2 text-xs"
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
삭제
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<LoadingSpinner />
|
||||
<span className="text-muted-foreground ml-2 text-sm">
|
||||
{getTextFromUI(TABLE_MANAGEMENT_KEYS.MESSAGE_LOADING_TABLES, "테이블 로딩 중...")}
|
||||
</span>
|
||||
</div>
|
||||
) : tables.length === 0 ? (
|
||||
<div className="text-muted-foreground py-8 text-center text-sm">
|
||||
{getTextFromUI(TABLE_MANAGEMENT_KEYS.MESSAGE_NO_TABLES, "테이블이 없습니다")}
|
||||
</div>
|
||||
) : (
|
||||
tables
|
||||
.filter(
|
||||
(table) =>
|
||||
table.tableName.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
(table.displayName && table.displayName.toLowerCase().includes(searchTerm.toLowerCase())),
|
||||
)
|
||||
.map((table) => (
|
||||
<div
|
||||
key={table.tableName}
|
||||
className={`bg-card rounded-lg p-4 shadow-sm transition-all ${
|
||||
selectedTable === table.tableName
|
||||
? "bg-muted/30 shadow-md"
|
||||
: "hover:bg-muted/20 hover:shadow-lg"
|
||||
}`}
|
||||
style={
|
||||
selectedTable === table.tableName
|
||||
? { border: "2px solid #000000" }
|
||||
: { border: "2px solid transparent" }
|
||||
}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
{/* 체크박스 (최고 관리자만) */}
|
||||
{isSuperAdmin && (
|
||||
<Checkbox
|
||||
checked={selectedTableIds.has(table.tableName)}
|
||||
onCheckedChange={(checked) => handleTableCheck(table.tableName, checked as boolean)}
|
||||
aria-label={`${table.displayName || table.tableName} 선택`}
|
||||
className="mt-0.5"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
)}
|
||||
<div className="flex-1 cursor-pointer" onClick={() => handleTableSelect(table.tableName)}>
|
||||
<h4 className="text-sm font-semibold">{table.displayName || table.tableName}</h4>
|
||||
<p className="text-muted-foreground mt-1 text-xs">
|
||||
{table.description || getTextFromUI(TABLE_MANAGEMENT_KEYS.TABLE_DESCRIPTION, "설명 없음")}
|
||||
</p>
|
||||
<div className="mt-2 flex items-center justify-between border-t pt-2">
|
||||
<span className="text-muted-foreground text-xs">컬럼</span>
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{table.columnCount}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
right={
|
||||
<div className="flex h-full flex-col overflow-hidden">
|
||||
{!selectedTable ? (
|
||||
<div className="bg-card flex h-64 flex-col items-center justify-center rounded-lg border">
|
||||
<div className="flex flex-col items-center gap-2 text-center">
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{getTextFromUI(TABLE_MANAGEMENT_KEYS.SELECT_TABLE_PLACEHOLDER, "테이블을 선택해주세요")}
|
||||
</p>
|
||||
</div>
|
||||
{columnsLoading ? (
|
||||
<div className="flex flex-1 items-center justify-center">
|
||||
<LoadingSpinner />
|
||||
<span className="text-muted-foreground ml-2 text-sm">
|
||||
{getTextFromUI(TABLE_MANAGEMENT_KEYS.MESSAGE_LOADING_COLUMNS, "컬럼 정보 로딩 중...")}
|
||||
</span>
|
||||
</div>
|
||||
) : columns.length === 0 ? (
|
||||
<div className="text-muted-foreground flex flex-1 items-center justify-center text-sm">
|
||||
{getTextFromUI(TABLE_MANAGEMENT_KEYS.MESSAGE_NO_COLUMNS, "컬럼이 없습니다")}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* 테이블 라벨 설정 + 저장 버튼 (고정 영역) */}
|
||||
<div className="mb-4 flex items-center gap-4">
|
||||
<div className="flex-1">
|
||||
<Input
|
||||
value={tableLabel}
|
||||
onChange={(e) => setTableLabel(e.target.value)}
|
||||
placeholder="테이블 표시명"
|
||||
className="h-10 text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<Input
|
||||
value={tableDescription}
|
||||
onChange={(e) => setTableDescription(e.target.value)}
|
||||
placeholder="테이블 설명"
|
||||
className="h-10 text-sm"
|
||||
/>
|
||||
</div>
|
||||
{/* 저장 버튼 (항상 보이도록 상단에 배치) */}
|
||||
<Button
|
||||
onClick={saveAllSettings}
|
||||
disabled={!selectedTable || columns.length === 0 || isSaving}
|
||||
className="h-10 gap-2 text-sm font-medium"
|
||||
>
|
||||
{isSaving ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Settings className="h-4 w-4" />
|
||||
)}
|
||||
{isSaving ? "저장 중..." : "전체 설정 저장"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{columnsLoading ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<LoadingSpinner />
|
||||
<span className="text-muted-foreground ml-2 text-sm">
|
||||
{getTextFromUI(TABLE_MANAGEMENT_KEYS.MESSAGE_LOADING_COLUMNS, "컬럼 정보 로딩 중...")}
|
||||
</span>
|
||||
</div>
|
||||
) : columns.length === 0 ? (
|
||||
<div className="text-muted-foreground py-8 text-center text-sm">
|
||||
{getTextFromUI(TABLE_MANAGEMENT_KEYS.MESSAGE_NO_COLUMNS, "컬럼이 없습니다")}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-1 overflow-hidden">
|
||||
<div className="flex min-w-0 flex-1 flex-col overflow-hidden">
|
||||
<TypeOverviewStrip
|
||||
columns={columns}
|
||||
activeFilter={typeFilter}
|
||||
onFilterChange={setTypeFilter}
|
||||
/>
|
||||
<ColumnGrid
|
||||
columns={columns}
|
||||
selectedColumn={selectedColumn}
|
||||
onSelectColumn={setSelectedColumn}
|
||||
onColumnChange={(columnName, field, value) => {
|
||||
const idx = columns.findIndex((c) => c.columnName === columnName);
|
||||
if (idx >= 0) handleColumnChange(idx, field, value);
|
||||
}}
|
||||
constraints={constraints}
|
||||
typeFilter={typeFilter}
|
||||
getColumnIndexState={getColumnIndexState}
|
||||
/>
|
||||
</div>
|
||||
{selectedColumn && (
|
||||
<div className="w-[360px] flex-shrink-0 overflow-hidden">
|
||||
<ColumnDetailPanel
|
||||
column={columns.find((c) => c.columnName === selectedColumn) ?? null}
|
||||
tables={tables}
|
||||
referenceTableColumns={referenceTableColumns}
|
||||
secondLevelMenus={secondLevelMenus}
|
||||
numberingRules={numberingRules}
|
||||
onColumnChange={(field, value) => {
|
||||
if (!selectedColumn) return;
|
||||
if (field === "inputType") {
|
||||
handleInputTypeChange(selectedColumn, value as string);
|
||||
return;
|
||||
}
|
||||
if (field === "referenceTable" && value) {
|
||||
loadReferenceTableColumns(value as string);
|
||||
}
|
||||
setColumns((prev) =>
|
||||
prev.map((c) =>
|
||||
c.columnName === selectedColumn ? { ...c, [field]: value } : c,
|
||||
),
|
||||
);
|
||||
}}
|
||||
onClose={() => setSelectedColumn(null)}
|
||||
onLoadReferenceColumns={loadReferenceTableColumns}
|
||||
codeCategoryOptions={commonCodeOptions}
|
||||
referenceTableOptions={referenceTableOptions}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<TypeOverviewStrip
|
||||
columns={columns}
|
||||
activeFilter={typeFilter}
|
||||
onFilterChange={setTypeFilter}
|
||||
/>
|
||||
<ColumnGrid
|
||||
columns={columns}
|
||||
selectedColumn={selectedColumn}
|
||||
onSelectColumn={setSelectedColumn}
|
||||
onColumnChange={(columnName, field, value) => {
|
||||
const idx = columns.findIndex((c) => c.columnName === columnName);
|
||||
if (idx >= 0) handleColumnChange(idx, field, value);
|
||||
}}
|
||||
constraints={constraints}
|
||||
typeFilter={typeFilter}
|
||||
getColumnIndexState={getColumnIndexState}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
leftTitle="테이블 목록"
|
||||
leftWidth={20}
|
||||
minLeftWidth={10}
|
||||
maxLeftWidth={35}
|
||||
height="100%"
|
||||
className="flex-1 overflow-hidden"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 우측: 상세 패널 (selectedColumn 있을 때만) */}
|
||||
{selectedColumn && (
|
||||
<div className="w-[320px] min-w-[320px] flex-shrink-0 overflow-hidden">
|
||||
<ColumnDetailPanel
|
||||
column={columns.find((c) => c.columnName === selectedColumn) ?? null}
|
||||
tables={tables}
|
||||
referenceTableColumns={referenceTableColumns}
|
||||
secondLevelMenus={secondLevelMenus}
|
||||
numberingRules={numberingRules}
|
||||
onColumnChange={(field, value) => {
|
||||
if (!selectedColumn) return;
|
||||
if (field === "inputType") {
|
||||
handleInputTypeChange(selectedColumn, value as string);
|
||||
return;
|
||||
}
|
||||
if (field === "referenceTable" && value) {
|
||||
loadReferenceTableColumns(value as string);
|
||||
}
|
||||
setColumns((prev) =>
|
||||
prev.map((c) =>
|
||||
c.columnName === selectedColumn ? { ...c, [field]: value } : c,
|
||||
),
|
||||
);
|
||||
}}
|
||||
onClose={() => setSelectedColumn(null)}
|
||||
onLoadReferenceColumns={loadReferenceTableColumns}
|
||||
codeCategoryOptions={commonCodeOptions}
|
||||
referenceTableOptions={referenceTableOptions}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* DDL 모달 컴포넌트들 */}
|
||||
{isSuperAdmin && (
|
||||
|
|
@ -1817,7 +1823,6 @@ export default function TableManagementPage() {
|
|||
|
||||
{/* Scroll to Top 버튼 */}
|
||||
<ScrollToTop />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -100,75 +100,152 @@ export function ColumnDetailPanel({
|
|||
<div className="flex-1 space-y-4 overflow-y-auto p-4">
|
||||
{/* [섹션 1] 데이터 타입 선택 */}
|
||||
<section className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Type className="h-4 w-4 text-muted-foreground" />
|
||||
<Label className="text-sm font-medium">데이터 타입</Label>
|
||||
<div>
|
||||
<p className="text-sm font-semibold">이 필드는 어떤 유형인가요?</p>
|
||||
<p className="text-xs text-muted-foreground">유형에 따라 입력 방식이 바뀌어요</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{Object.entries(INPUT_TYPE_COLORS).map(([type, conf]) => (
|
||||
<button
|
||||
key={type}
|
||||
type="button"
|
||||
onClick={() => onColumnChange("inputType", type)}
|
||||
className={cn(
|
||||
"rounded-lg border p-2 text-left text-xs transition-colors",
|
||||
conf.bgColor,
|
||||
conf.color,
|
||||
(column.inputType || "text") === type
|
||||
? "ring-2 ring-ring"
|
||||
: "border-border hover:bg-muted/50",
|
||||
)}
|
||||
>
|
||||
{conf.label}
|
||||
</button>
|
||||
))}
|
||||
<div className="grid grid-cols-3 gap-1.5">
|
||||
{Object.entries(INPUT_TYPE_COLORS).map(([type, conf]) => {
|
||||
const isSelected = (column.inputType || "text") === type;
|
||||
return (
|
||||
<button
|
||||
key={type}
|
||||
type="button"
|
||||
onClick={() => onColumnChange("inputType", type)}
|
||||
className={cn(
|
||||
"flex flex-col items-center gap-1 rounded-lg border px-1.5 py-2.5 text-center transition-all",
|
||||
isSelected
|
||||
? "border-primary bg-primary/5 ring-1 ring-primary/30"
|
||||
: "border-border hover:border-primary/30 hover:bg-accent/50",
|
||||
)}
|
||||
>
|
||||
<span className={cn(
|
||||
"text-base font-bold leading-none",
|
||||
isSelected ? "text-primary" : conf.color,
|
||||
)}>
|
||||
{conf.iconChar}
|
||||
</span>
|
||||
<span className={cn(
|
||||
"text-[11px] font-semibold leading-tight",
|
||||
isSelected ? "text-primary" : "text-foreground",
|
||||
)}>
|
||||
{conf.label}
|
||||
</span>
|
||||
<span className="text-[9px] leading-tight text-muted-foreground">
|
||||
{conf.desc}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* [섹션 2] 타입별 상세 설정 */}
|
||||
{column.inputType === "entity" && (
|
||||
<section className="space-y-2">
|
||||
<section className="space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Settings2 className="h-4 w-4 text-muted-foreground" />
|
||||
<Label className="text-sm font-medium">엔티티 참조</Label>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs text-muted-foreground">참조 테이블</Label>
|
||||
<Popover open={entityTableOpen} onOpenChange={setEntityTableOpen}>
|
||||
|
||||
{/* 참조 테이블 */}
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[11px] font-medium text-muted-foreground">참조 테이블</Label>
|
||||
<Popover open={entityTableOpen} onOpenChange={setEntityTableOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
className="h-9 w-full justify-between text-xs"
|
||||
>
|
||||
{column.referenceTable && column.referenceTable !== "none"
|
||||
? refTableOpts.find((o) => o.value === column.referenceTable)?.label ?? column.referenceTable
|
||||
: "테이블 선택..."}
|
||||
<ChevronsUpDown className="ml-2 h-3 w-3 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[280px] p-0" align="start">
|
||||
<Command>
|
||||
<CommandInput placeholder="테이블 검색..." className="h-8 text-xs" />
|
||||
<CommandList className="max-h-[200px]">
|
||||
<CommandEmpty className="py-2 text-center text-xs">테이블을 찾을 수 없습니다.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{refTableOpts.map((opt) => (
|
||||
<CommandItem
|
||||
key={opt.value}
|
||||
value={`${opt.label} ${opt.value}`}
|
||||
onSelect={() => {
|
||||
onColumnChange("referenceTable", opt.value === "none" ? undefined : opt.value);
|
||||
if (opt.value !== "none") onLoadReferenceColumns?.(opt.value);
|
||||
setEntityTableOpen(false);
|
||||
}}
|
||||
className="text-xs"
|
||||
>
|
||||
<Check
|
||||
className={cn("mr-2 h-3 w-3", column.referenceTable === opt.value ? "opacity-100" : "opacity-0")}
|
||||
/>
|
||||
{opt.label}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
{/* 조인 컬럼 */}
|
||||
{column.referenceTable && column.referenceTable !== "none" && (
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[11px] font-medium text-muted-foreground">조인 컬럼(값)</Label>
|
||||
<Popover open={entityColumnOpen} onOpenChange={setEntityColumnOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
disabled={refColumns.length === 0}
|
||||
className="h-9 w-full justify-between text-xs"
|
||||
>
|
||||
{column.referenceTable && column.referenceTable !== "none"
|
||||
? refTableOpts.find((o) => o.value === column.referenceTable)?.label ?? column.referenceTable
|
||||
: "테이블 선택..."}
|
||||
{column.referenceColumn && column.referenceColumn !== "none"
|
||||
? column.referenceColumn
|
||||
: "컬럼 선택..."}
|
||||
<ChevronsUpDown className="ml-2 h-3 w-3 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[280px] p-0" align="start">
|
||||
<Command>
|
||||
<CommandInput placeholder="테이블 검색..." className="h-8 text-xs" />
|
||||
<CommandInput placeholder="컬럼 검색..." className="h-8 text-xs" />
|
||||
<CommandList className="max-h-[200px]">
|
||||
<CommandEmpty className="py-2 text-center text-xs">테이블을 찾을 수 없습니다.</CommandEmpty>
|
||||
<CommandEmpty className="py-2 text-center text-xs">컬럼을 찾을 수 없습니다.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{refTableOpts.map((opt) => (
|
||||
<CommandItem
|
||||
value="none"
|
||||
onSelect={() => {
|
||||
onColumnChange("referenceColumn", undefined);
|
||||
setEntityColumnOpen(false);
|
||||
}}
|
||||
className="text-xs"
|
||||
>
|
||||
<Check className={cn("mr-2 h-3 w-3", !column.referenceColumn ? "opacity-100" : "opacity-0")} />
|
||||
선택 안함
|
||||
</CommandItem>
|
||||
{refColumns.map((refCol) => (
|
||||
<CommandItem
|
||||
key={opt.value}
|
||||
value={`${opt.label} ${opt.value}`}
|
||||
key={refCol.columnName}
|
||||
value={`${refCol.displayName ?? ""} ${refCol.columnName}`}
|
||||
onSelect={() => {
|
||||
onColumnChange("referenceTable", opt.value === "none" ? undefined : opt.value);
|
||||
if (opt.value !== "none") onLoadReferenceColumns?.(opt.value);
|
||||
setEntityTableOpen(false);
|
||||
onColumnChange("referenceColumn", refCol.columnName);
|
||||
setEntityColumnOpen(false);
|
||||
}}
|
||||
className="text-xs"
|
||||
>
|
||||
<Check
|
||||
className={cn("mr-2 h-3 w-3", column.referenceTable === opt.value ? "opacity-100" : "opacity-0")}
|
||||
className={cn(
|
||||
"mr-2 h-3 w-3",
|
||||
column.referenceColumn === refCol.columnName ? "opacity-100" : "opacity-0",
|
||||
)}
|
||||
/>
|
||||
{opt.label}
|
||||
{refCol.columnName}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
|
|
@ -177,67 +254,20 @@ export function ColumnDetailPanel({
|
|||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
{column.referenceTable && column.referenceTable !== "none" && (
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs text-muted-foreground">조인 컬럼(값)</Label>
|
||||
<Popover open={entityColumnOpen} onOpenChange={setEntityColumnOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
disabled={refColumns.length === 0}
|
||||
className="h-9 w-full justify-between text-xs"
|
||||
>
|
||||
{column.referenceColumn && column.referenceColumn !== "none"
|
||||
? column.referenceColumn
|
||||
: "컬럼 선택..."}
|
||||
<ChevronsUpDown className="ml-2 h-3 w-3 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[280px] p-0" align="start">
|
||||
<Command>
|
||||
<CommandInput placeholder="컬럼 검색..." className="h-8 text-xs" />
|
||||
<CommandList className="max-h-[200px]">
|
||||
<CommandEmpty className="py-2 text-center text-xs">컬럼을 찾을 수 없습니다.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
<CommandItem
|
||||
value="none"
|
||||
onSelect={() => {
|
||||
onColumnChange("referenceColumn", undefined);
|
||||
setEntityColumnOpen(false);
|
||||
}}
|
||||
className="text-xs"
|
||||
>
|
||||
<Check className={cn("mr-2 h-3 w-3", !column.referenceColumn ? "opacity-100" : "opacity-0")} />
|
||||
선택 안함
|
||||
</CommandItem>
|
||||
{refColumns.map((refCol) => (
|
||||
<CommandItem
|
||||
key={refCol.columnName}
|
||||
value={`${refCol.displayName ?? ""} ${refCol.columnName}`}
|
||||
onSelect={() => {
|
||||
onColumnChange("referenceColumn", refCol.columnName);
|
||||
setEntityColumnOpen(false);
|
||||
}}
|
||||
className="text-xs"
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-3 w-3",
|
||||
column.referenceColumn === refCol.columnName ? "opacity-100" : "opacity-0",
|
||||
)}
|
||||
/>
|
||||
{refCol.columnName}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 참조 요약 미니맵 */}
|
||||
{column.referenceTable && column.referenceTable !== "none" && column.referenceColumn && (
|
||||
<div className="flex items-center gap-2 rounded-md bg-violet-50 px-3 py-2">
|
||||
<span className="font-mono text-[11px] font-semibold text-violet-600">
|
||||
{column.referenceTable}
|
||||
</span>
|
||||
<span className="text-muted-foreground text-[10px]">→</span>
|
||||
<span className="font-mono text-[11px] font-semibold text-violet-600">
|
||||
{column.referenceColumn}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ export function ColumnGrid({
|
|||
columns,
|
||||
selectedColumn,
|
||||
onSelectColumn,
|
||||
onColumnChange,
|
||||
constraints,
|
||||
typeFilter = null,
|
||||
getColumnIndexState: externalGetIndexState,
|
||||
|
|
@ -128,8 +129,8 @@ export function ColumnGrid({
|
|||
isSelected && "border-primary/30 bg-primary/5 shadow-sm",
|
||||
)}
|
||||
>
|
||||
{/* 4px 색상바 */}
|
||||
<div className={cn("h-full min-h-8 w-1 rounded-full", typeConf.bgColor, typeConf.color)} />
|
||||
{/* 4px 색상바 (타입별 진한 색) */}
|
||||
<div className={cn("h-full min-h-8 w-1 rounded-full", typeConf.barColor)} />
|
||||
|
||||
{/* 라벨 + 컬럼명 */}
|
||||
<div className="min-w-0">
|
||||
|
|
@ -180,48 +181,72 @@ export function ColumnGrid({
|
|||
{typeConf.label}
|
||||
</div>
|
||||
|
||||
{/* PK / NN / IDX / UQ (읽기 전용) */}
|
||||
{/* PK / NN / IDX / UQ (클릭 토글) */}
|
||||
<div className="flex flex-wrap items-center justify-center gap-1">
|
||||
<span
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
"rounded border px-1.5 py-0.5 text-[10px] font-medium",
|
||||
"rounded border px-1.5 py-0.5 text-[10px] font-bold transition-colors",
|
||||
idxState.isPk
|
||||
? "border-primary/30 bg-primary/10 text-primary"
|
||||
: "border-border bg-muted/50 text-muted-foreground",
|
||||
? "border-blue-200 bg-blue-50 text-blue-600"
|
||||
: "border-border text-muted-foreground/40 hover:border-blue-200 hover:text-blue-400",
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onColumnChange(column.columnName, "isPrimaryKey" as keyof ColumnTypeInfo, !idxState.isPk);
|
||||
}}
|
||||
title="Primary Key 토글"
|
||||
>
|
||||
PK
|
||||
</span>
|
||||
<span
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
"rounded border px-1.5 py-0.5 text-[10px] font-medium",
|
||||
"rounded border px-1.5 py-0.5 text-[10px] font-bold transition-colors",
|
||||
column.isNullable === "NO"
|
||||
? "border-amber-200 bg-amber-50 text-amber-600 dark:border-amber-900/50 dark:bg-amber-950/40 dark:text-amber-400"
|
||||
: "border-border bg-muted/50 text-muted-foreground",
|
||||
? "border-amber-200 bg-amber-50 text-amber-600"
|
||||
: "border-border text-muted-foreground/40 hover:border-amber-200 hover:text-amber-400",
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onColumnChange(column.columnName, "isNullable", column.isNullable === "NO" ? "YES" : "NO");
|
||||
}}
|
||||
title="Not Null 토글"
|
||||
>
|
||||
NN
|
||||
</span>
|
||||
<span
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
"rounded border px-1.5 py-0.5 text-[10px] font-medium",
|
||||
"rounded border px-1.5 py-0.5 text-[10px] font-bold transition-colors",
|
||||
idxState.hasIndex
|
||||
? "border-emerald-200 bg-emerald-50 text-emerald-600 dark:border-emerald-900/50 dark:bg-emerald-950/40 dark:text-emerald-400"
|
||||
: "border-border bg-muted/50 text-muted-foreground",
|
||||
? "border-emerald-200 bg-emerald-50 text-emerald-600"
|
||||
: "border-border text-muted-foreground/40 hover:border-emerald-200 hover:text-emerald-400",
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onColumnChange(column.columnName, "hasIndex" as keyof ColumnTypeInfo, !idxState.hasIndex);
|
||||
}}
|
||||
title="Index 토글"
|
||||
>
|
||||
IDX
|
||||
</span>
|
||||
<span
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
"rounded border px-1.5 py-0.5 text-[10px] font-medium",
|
||||
"rounded border px-1.5 py-0.5 text-[10px] font-bold transition-colors",
|
||||
column.isUnique === "YES"
|
||||
? "border-violet-200 bg-violet-50 text-violet-600 dark:border-violet-900/50 dark:bg-violet-950/40 dark:text-violet-400"
|
||||
: "border-border bg-muted/50 text-muted-foreground",
|
||||
? "border-violet-200 bg-violet-50 text-violet-600"
|
||||
: "border-border text-muted-foreground/40 hover:border-violet-200 hover:text-violet-400",
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onColumnChange(column.columnName, "isUnique", column.isUnique === "YES" ? "NO" : "YES");
|
||||
}}
|
||||
title="Unique 토글"
|
||||
>
|
||||
UQ
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-center">
|
||||
|
|
|
|||
|
|
@ -47,23 +47,25 @@ export type ColumnGroup = "basic" | "reference" | "meta";
|
|||
export interface TypeColorConfig {
|
||||
color: string;
|
||||
bgColor: string;
|
||||
barColor: string;
|
||||
label: string;
|
||||
icon?: string;
|
||||
desc: string;
|
||||
iconChar: string;
|
||||
}
|
||||
|
||||
/** 입력 타입별 색상 맵 - 배경/텍스트/보더는 다크에서 자동 변환 */
|
||||
/** 입력 타입별 색상 맵 - iconChar는 카드 선택용 시각 아이콘 */
|
||||
export const INPUT_TYPE_COLORS: Record<string, TypeColorConfig> = {
|
||||
text: { color: "text-slate-600", bgColor: "bg-slate-50", label: "텍스트" },
|
||||
number: { color: "text-indigo-600", bgColor: "bg-indigo-50", label: "숫자" },
|
||||
date: { color: "text-amber-600", bgColor: "bg-amber-50", label: "날짜" },
|
||||
code: { color: "text-emerald-600", bgColor: "bg-emerald-50", label: "코드" },
|
||||
entity: { color: "text-violet-600", bgColor: "bg-violet-50", label: "엔티티" },
|
||||
select: { color: "text-cyan-600", bgColor: "bg-cyan-50", label: "셀렉트" },
|
||||
checkbox: { color: "text-pink-600", bgColor: "bg-pink-50", label: "체크박스" },
|
||||
numbering: { color: "text-orange-600", bgColor: "bg-orange-50", label: "채번" },
|
||||
category: { color: "text-teal-600", bgColor: "bg-teal-50", label: "카테고리" },
|
||||
textarea: { color: "text-indigo-600", bgColor: "bg-indigo-50", label: "텍스트영역" },
|
||||
radio: { color: "text-rose-600", bgColor: "bg-rose-50", label: "라디오" },
|
||||
text: { color: "text-slate-600", bgColor: "bg-slate-50", barColor: "bg-slate-400", label: "텍스트", desc: "일반 텍스트 입력", iconChar: "T" },
|
||||
number: { color: "text-indigo-600", bgColor: "bg-indigo-50", barColor: "bg-indigo-500", label: "숫자", desc: "숫자만 입력", iconChar: "#" },
|
||||
date: { color: "text-amber-600", bgColor: "bg-amber-50", barColor: "bg-amber-500", label: "날짜", desc: "날짜 선택", iconChar: "D" },
|
||||
code: { color: "text-emerald-600", bgColor: "bg-emerald-50", barColor: "bg-emerald-500", label: "코드", desc: "공통코드 선택", iconChar: "{}" },
|
||||
entity: { color: "text-violet-600", bgColor: "bg-violet-50", barColor: "bg-violet-500", label: "테이블 참조", desc: "다른 테이블 연결", iconChar: "⊞" },
|
||||
select: { color: "text-cyan-600", bgColor: "bg-cyan-50", barColor: "bg-cyan-500", label: "셀렉트", desc: "직접 옵션 선택", iconChar: "☰" },
|
||||
checkbox: { color: "text-pink-600", bgColor: "bg-pink-50", barColor: "bg-pink-500", label: "체크박스", desc: "예/아니오 선택", iconChar: "☑" },
|
||||
numbering: { color: "text-orange-600", bgColor: "bg-orange-50", barColor: "bg-orange-500", label: "채번", desc: "자동 번호 생성", iconChar: "≡" },
|
||||
category: { color: "text-teal-600", bgColor: "bg-teal-50", barColor: "bg-teal-500", label: "카테고리", desc: "등록된 선택지", iconChar: "⊟" },
|
||||
textarea: { color: "text-indigo-600", bgColor: "bg-indigo-50", barColor: "bg-indigo-400", label: "여러 줄", desc: "긴 텍스트 입력", iconChar: "≡" },
|
||||
radio: { color: "text-rose-600", bgColor: "bg-rose-50", barColor: "bg-rose-500", label: "라디오", desc: "하나만 선택", iconChar: "◉" },
|
||||
};
|
||||
|
||||
/** 컬럼 그룹 판별 */
|
||||
|
|
|
|||
Loading…
Reference in New Issue