[agent-pipeline] pipe-20260316081628-53mz round-1
This commit is contained in:
parent
825f164bde
commit
a391918e58
|
|
@ -50,43 +50,10 @@ import {
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
|
import type { TableInfo, ColumnTypeInfo, SecondLevelMenu } from "@/components/admin/table-type/types";
|
||||||
interface TableInfo {
|
import { TypeOverviewStrip } from "@/components/admin/table-type/TypeOverviewStrip";
|
||||||
tableName: string;
|
import { ColumnGrid } from "@/components/admin/table-type/ColumnGrid";
|
||||||
displayName: string;
|
import { ColumnDetailPanel } from "@/components/admin/table-type/ColumnDetailPanel";
|
||||||
description: string;
|
|
||||||
columnCount: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ColumnTypeInfo {
|
|
||||||
columnName: string;
|
|
||||||
displayName: string;
|
|
||||||
inputType: string; // webType → inputType 변경
|
|
||||||
detailSettings: string;
|
|
||||||
description: string;
|
|
||||||
isNullable: string;
|
|
||||||
isUnique: string;
|
|
||||||
defaultValue?: string;
|
|
||||||
maxLength?: number;
|
|
||||||
numericPrecision?: number;
|
|
||||||
numericScale?: number;
|
|
||||||
codeCategory?: string;
|
|
||||||
codeValue?: string;
|
|
||||||
referenceTable?: string;
|
|
||||||
referenceColumn?: string;
|
|
||||||
displayColumn?: string; // 🎯 Entity 조인에서 표시할 컬럼명
|
|
||||||
categoryMenus?: number[];
|
|
||||||
hierarchyRole?: "large" | "medium" | "small";
|
|
||||||
numberingRuleId?: string;
|
|
||||||
categoryRef?: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SecondLevelMenu {
|
|
||||||
menuObjid: number;
|
|
||||||
menuName: string;
|
|
||||||
parentMenuName: string;
|
|
||||||
screenCode?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function TableManagementPage() {
|
export default function TableManagementPage() {
|
||||||
const { userLang, getText } = useMultiLang({ companyCode: "*" });
|
const { userLang, getText } = useMultiLang({ companyCode: "*" });
|
||||||
|
|
@ -164,6 +131,11 @@ export default function TableManagementPage() {
|
||||||
// 선택된 테이블 목록 (체크박스)
|
// 선택된 테이블 목록 (체크박스)
|
||||||
const [selectedTableIds, setSelectedTableIds] = useState<Set<string>>(new Set());
|
const [selectedTableIds, setSelectedTableIds] = useState<Set<string>>(new Set());
|
||||||
|
|
||||||
|
// 컬럼 그리드: 선택된 컬럼(우측 상세 패널 표시)
|
||||||
|
const [selectedColumn, setSelectedColumn] = useState<string | null>(null);
|
||||||
|
// 타입 오버뷰 스트립: 타입 필터 (null = 전체)
|
||||||
|
const [typeFilter, setTypeFilter] = useState<string | null>(null);
|
||||||
|
|
||||||
// 최고 관리자 여부 확인 (회사코드가 "*" AND userType이 "SUPER_ADMIN")
|
// 최고 관리자 여부 확인 (회사코드가 "*" AND userType이 "SUPER_ADMIN")
|
||||||
const isSuperAdmin = user?.companyCode === "*" && user?.userType === "SUPER_ADMIN";
|
const isSuperAdmin = user?.companyCode === "*" && user?.userType === "SUPER_ADMIN";
|
||||||
|
|
||||||
|
|
@ -442,6 +414,8 @@ export default function TableManagementPage() {
|
||||||
setSelectedTable(tableName);
|
setSelectedTable(tableName);
|
||||||
setCurrentPage(1);
|
setCurrentPage(1);
|
||||||
setColumns([]);
|
setColumns([]);
|
||||||
|
setSelectedColumn(null);
|
||||||
|
setTypeFilter(null);
|
||||||
|
|
||||||
// 선택된 테이블 정보에서 라벨 설정
|
// 선택된 테이블 정보에서 라벨 설정
|
||||||
const tableInfo = tables.find((table) => table.tableName === tableName);
|
const tableInfo = tables.find((table) => table.tableName === tableName);
|
||||||
|
|
@ -1588,418 +1562,57 @@ export default function TableManagementPage() {
|
||||||
{getTextFromUI(TABLE_MANAGEMENT_KEYS.MESSAGE_NO_COLUMNS, "컬럼이 없습니다")}
|
{getTextFromUI(TABLE_MANAGEMENT_KEYS.MESSAGE_NO_COLUMNS, "컬럼이 없습니다")}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-1 flex-col overflow-hidden">
|
<div className="flex flex-1 overflow-hidden">
|
||||||
{/* 컬럼 헤더 (고정) */}
|
<div className="flex min-w-0 flex-1 flex-col overflow-hidden">
|
||||||
<div
|
<TypeOverviewStrip
|
||||||
className="text-foreground grid h-12 flex-shrink-0 items-center border-b px-6 py-3 text-sm font-semibold"
|
columns={columns}
|
||||||
style={{ gridTemplateColumns: "160px 200px 250px minmax(80px,1fr) 70px 70px 70px 70px" }}
|
activeFilter={typeFilter}
|
||||||
>
|
onFilterChange={setTypeFilter}
|
||||||
<div className="pr-4">라벨</div>
|
/>
|
||||||
<div className="px-4">컬럼명</div>
|
<ColumnGrid
|
||||||
<div className="pr-6">입력 타입</div>
|
columns={columns}
|
||||||
<div className="pl-4">설명</div>
|
selectedColumn={selectedColumn}
|
||||||
<div className="text-center text-xs">Primary</div>
|
onSelectColumn={setSelectedColumn}
|
||||||
<div className="text-center text-xs">NotNull</div>
|
onColumnChange={(columnName, field, value) => {
|
||||||
<div className="text-center text-xs">Index</div>
|
const idx = columns.findIndex((c) => c.columnName === columnName);
|
||||||
<div className="text-center text-xs">Unique</div>
|
if (idx >= 0) handleColumnChange(idx, field, value);
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 컬럼 리스트 (스크롤 영역) */}
|
|
||||||
<div
|
|
||||||
className="flex-1 overflow-y-auto"
|
|
||||||
onScroll={(e) => {
|
|
||||||
const { scrollTop, scrollHeight, clientHeight } = e.currentTarget;
|
|
||||||
// 스크롤이 끝에 가까워지면 더 많은 데이터 로드
|
|
||||||
if (scrollHeight - scrollTop <= clientHeight + 100) {
|
|
||||||
loadMoreColumns();
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
>
|
constraints={constraints}
|
||||||
{columns.map((column, index) => {
|
typeFilter={typeFilter}
|
||||||
const idxState = getColumnIndexState(column.columnName);
|
getColumnIndexState={getColumnIndexState}
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={column.columnName}
|
|
||||||
className="bg-background hover:bg-muted/50 grid min-h-16 items-start border-b px-6 py-3 transition-colors"
|
|
||||||
style={{ gridTemplateColumns: "160px 200px 250px minmax(80px,1fr) 70px 70px 70px 70px" }}
|
|
||||||
>
|
|
||||||
<div className="pr-4">
|
|
||||||
<Input
|
|
||||||
value={column.displayName || ""}
|
|
||||||
onChange={(e) => handleLabelChange(column.columnName, e.target.value)}
|
|
||||||
placeholder={column.columnName}
|
|
||||||
className="h-8 text-xs"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="px-4 pt-1">
|
{selectedColumn && (
|
||||||
<div className="font-mono text-sm">{column.columnName}</div>
|
<div className="w-[360px] flex-shrink-0 overflow-hidden">
|
||||||
</div>
|
<ColumnDetailPanel
|
||||||
<div className="pr-6">
|
column={columns.find((c) => c.columnName === selectedColumn) ?? null}
|
||||||
<div className="space-y-3">
|
tables={tables}
|
||||||
{/* 입력 타입 선택 */}
|
referenceTableColumns={referenceTableColumns}
|
||||||
<Select
|
secondLevelMenus={secondLevelMenus}
|
||||||
value={column.inputType || "text"}
|
numberingRules={numberingRules}
|
||||||
onValueChange={(value) => handleInputTypeChange(column.columnName, value)}
|
onColumnChange={(field, value) => {
|
||||||
>
|
if (!selectedColumn) return;
|
||||||
<SelectTrigger className="h-8 text-xs">
|
if (field === "inputType") {
|
||||||
<SelectValue placeholder="입력 타입 선택" />
|
handleInputTypeChange(selectedColumn, value as string);
|
||||||
</SelectTrigger>
|
return;
|
||||||
<SelectContent>
|
|
||||||
{memoizedInputTypeOptions.map((option) => (
|
|
||||||
<SelectItem key={option.value} value={option.value}>
|
|
||||||
{option.label}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
{/* 입력 타입이 'code'인 경우 공통코드 선택 */}
|
|
||||||
{column.inputType === "code" && (
|
|
||||||
<>
|
|
||||||
<Select
|
|
||||||
value={column.codeCategory || "none"}
|
|
||||||
onValueChange={(value) =>
|
|
||||||
handleDetailSettingsChange(column.columnName, "code", value)
|
|
||||||
}
|
}
|
||||||
>
|
if (field === "referenceTable" && value) {
|
||||||
<SelectTrigger className="h-8 text-xs">
|
loadReferenceTableColumns(value as string);
|
||||||
<SelectValue placeholder="공통코드 선택" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{commonCodeOptions.map((option, index) => (
|
|
||||||
<SelectItem key={`code-${option.value}-${index}`} value={option.value}>
|
|
||||||
{option.label}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
{/* 계층구조 역할 선택 */}
|
|
||||||
{column.codeCategory && column.codeCategory !== "none" && (
|
|
||||||
<Select
|
|
||||||
value={column.hierarchyRole || "none"}
|
|
||||||
onValueChange={(value) =>
|
|
||||||
handleDetailSettingsChange(column.columnName, "hierarchy_role", value)
|
|
||||||
}
|
}
|
||||||
>
|
|
||||||
<SelectTrigger className="h-8 text-xs">
|
|
||||||
<SelectValue placeholder="계층 역할" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="none">일반</SelectItem>
|
|
||||||
<SelectItem value="large">대분류</SelectItem>
|
|
||||||
<SelectItem value="medium">중분류</SelectItem>
|
|
||||||
<SelectItem value="small">소분류</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{/* 카테고리 타입: 참조 설정 */}
|
|
||||||
{column.inputType === "category" && (
|
|
||||||
<div className="w-56">
|
|
||||||
<label className="text-muted-foreground mb-1 block text-xs">카테고리 참조 (선택)</label>
|
|
||||||
<Input
|
|
||||||
value={column.categoryRef || ""}
|
|
||||||
onChange={(e) => {
|
|
||||||
const val = e.target.value || null;
|
|
||||||
setColumns((prev) =>
|
setColumns((prev) =>
|
||||||
prev.map((c) =>
|
prev.map((c) =>
|
||||||
c.columnName === column.columnName
|
c.columnName === selectedColumn ? { ...c, [field]: value } : c,
|
||||||
? { ...c, categoryRef: val }
|
),
|
||||||
: c
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
placeholder="테이블명.컬럼명"
|
onClose={() => setSelectedColumn(null)}
|
||||||
className="h-8 text-xs"
|
onLoadReferenceColumns={loadReferenceTableColumns}
|
||||||
/>
|
codeCategoryOptions={commonCodeOptions}
|
||||||
<p className="text-muted-foreground mt-0.5 text-[10px]">
|
referenceTableOptions={referenceTableOptions}
|
||||||
다른 테이블의 카테고리 값 참조 시 입력
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{/* 입력 타입이 'entity'인 경우 참조 테이블 선택 */}
|
|
||||||
{column.inputType === "entity" && (
|
|
||||||
<>
|
|
||||||
{/* 참조 테이블 - 검색 가능한 Combobox */}
|
|
||||||
<div className="w-56">
|
|
||||||
<label className="text-muted-foreground mb-1 block text-xs">참조 테이블</label>
|
|
||||||
<Popover
|
|
||||||
open={entityComboboxOpen[column.columnName]?.table || false}
|
|
||||||
onOpenChange={(open) =>
|
|
||||||
setEntityComboboxOpen((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[column.columnName]: { ...prev[column.columnName], table: open },
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<PopoverTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
role="combobox"
|
|
||||||
aria-expanded={entityComboboxOpen[column.columnName]?.table || false}
|
|
||||||
className="bg-background h-8 w-full justify-between text-xs"
|
|
||||||
>
|
|
||||||
{column.referenceTable && column.referenceTable !== "none"
|
|
||||||
? referenceTableOptions.find((opt) => opt.value === column.referenceTable)
|
|
||||||
?.label || column.referenceTable
|
|
||||||
: "테이블 선택..."}
|
|
||||||
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 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>
|
|
||||||
{referenceTableOptions.map((option) => (
|
|
||||||
<CommandItem
|
|
||||||
key={option.value}
|
|
||||||
value={`${option.label} ${option.value}`}
|
|
||||||
onSelect={() => {
|
|
||||||
handleDetailSettingsChange(
|
|
||||||
column.columnName,
|
|
||||||
"entity",
|
|
||||||
option.value,
|
|
||||||
);
|
|
||||||
setEntityComboboxOpen((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[column.columnName]: {
|
|
||||||
...prev[column.columnName],
|
|
||||||
table: false,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
}}
|
|
||||||
className="text-xs"
|
|
||||||
>
|
|
||||||
<Check
|
|
||||||
className={cn(
|
|
||||||
"mr-2 h-3 w-3",
|
|
||||||
column.referenceTable === option.value
|
|
||||||
? "opacity-100"
|
|
||||||
: "opacity-0",
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<span className="font-medium">{option.label}</span>
|
|
||||||
{option.value !== "none" && (
|
|
||||||
<span className="text-muted-foreground text-[10px]">
|
|
||||||
{option.value}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</CommandItem>
|
|
||||||
))}
|
|
||||||
</CommandGroup>
|
|
||||||
</CommandList>
|
|
||||||
</Command>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 조인 컬럼 - 검색 가능한 Combobox */}
|
|
||||||
{column.referenceTable && column.referenceTable !== "none" && (
|
|
||||||
<div className="w-56">
|
|
||||||
<label className="text-muted-foreground mb-1 block text-xs">조인 컬럼</label>
|
|
||||||
<Popover
|
|
||||||
open={entityComboboxOpen[column.columnName]?.joinColumn || false}
|
|
||||||
onOpenChange={(open) =>
|
|
||||||
setEntityComboboxOpen((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[column.columnName]: { ...prev[column.columnName], joinColumn: open },
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<PopoverTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
role="combobox"
|
|
||||||
aria-expanded={entityComboboxOpen[column.columnName]?.joinColumn || false}
|
|
||||||
className="bg-background h-8 w-full justify-between text-xs"
|
|
||||||
disabled={
|
|
||||||
!referenceTableColumns[column.referenceTable] ||
|
|
||||||
referenceTableColumns[column.referenceTable].length === 0
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{!referenceTableColumns[column.referenceTable] ||
|
|
||||||
referenceTableColumns[column.referenceTable].length === 0 ? (
|
|
||||||
<span className="flex items-center gap-2">
|
|
||||||
<div className="border-primary h-3 w-3 animate-spin rounded-full border border-t-transparent"></div>
|
|
||||||
로딩중...
|
|
||||||
</span>
|
|
||||||
) : column.referenceColumn && column.referenceColumn !== "none" ? (
|
|
||||||
column.referenceColumn
|
|
||||||
) : (
|
|
||||||
"컬럼 선택..."
|
|
||||||
)}
|
|
||||||
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 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={() => {
|
|
||||||
handleDetailSettingsChange(
|
|
||||||
column.columnName,
|
|
||||||
"entity_reference_column",
|
|
||||||
"none",
|
|
||||||
);
|
|
||||||
setEntityComboboxOpen((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[column.columnName]: {
|
|
||||||
...prev[column.columnName],
|
|
||||||
joinColumn: false,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
}}
|
|
||||||
className="text-xs"
|
|
||||||
>
|
|
||||||
<Check
|
|
||||||
className={cn(
|
|
||||||
"mr-2 h-3 w-3",
|
|
||||||
column.referenceColumn === "none" || !column.referenceColumn
|
|
||||||
? "opacity-100"
|
|
||||||
: "opacity-0",
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
-- 선택 안함 --
|
|
||||||
</CommandItem>
|
|
||||||
{referenceTableColumns[column.referenceTable]?.map((refCol) => (
|
|
||||||
<CommandItem
|
|
||||||
key={refCol.columnName}
|
|
||||||
value={`${refCol.displayName || ""} ${refCol.columnName}`}
|
|
||||||
onSelect={() => {
|
|
||||||
handleDetailSettingsChange(
|
|
||||||
column.columnName,
|
|
||||||
"entity_reference_column",
|
|
||||||
refCol.columnName,
|
|
||||||
);
|
|
||||||
setEntityComboboxOpen((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[column.columnName]: {
|
|
||||||
...prev[column.columnName],
|
|
||||||
joinColumn: false,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
}}
|
|
||||||
className="text-xs"
|
|
||||||
>
|
|
||||||
<Check
|
|
||||||
className={cn(
|
|
||||||
"mr-2 h-3 w-3",
|
|
||||||
column.referenceColumn === refCol.columnName
|
|
||||||
? "opacity-100"
|
|
||||||
: "opacity-0",
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<span className="font-medium">{refCol.columnName}</span>
|
|
||||||
{refCol.displayName && (
|
|
||||||
<span className="text-muted-foreground text-[10px]">
|
|
||||||
{refCol.displayName}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</CommandItem>
|
|
||||||
))}
|
|
||||||
</CommandGroup>
|
|
||||||
</CommandList>
|
|
||||||
</Command>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 설정 완료 표시 */}
|
|
||||||
{column.referenceTable &&
|
|
||||||
column.referenceTable !== "none" &&
|
|
||||||
column.referenceColumn &&
|
|
||||||
column.referenceColumn !== "none" && (
|
|
||||||
<div className="bg-primary/10 text-primary flex items-center gap-1 rounded px-2 py-1 text-xs">
|
|
||||||
<Check className="h-3 w-3" />
|
|
||||||
<span className="truncate">설정 완료</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{/* 채번 타입은 옵션설정 > 채번설정에서 관리 (별도 선택 불필요) */}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="pl-4">
|
|
||||||
<Input
|
|
||||||
value={column.description || ""}
|
|
||||||
onChange={(e) => handleColumnChange(index, "description", e.target.value)}
|
|
||||||
placeholder="설명"
|
|
||||||
className="h-8 w-full text-xs"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/* PK 체크박스 */}
|
|
||||||
<div className="flex items-center justify-center pt-1">
|
|
||||||
<Checkbox
|
|
||||||
checked={idxState.isPk}
|
|
||||||
onCheckedChange={(checked) =>
|
|
||||||
handlePkToggle(column.columnName, checked as boolean)
|
|
||||||
}
|
|
||||||
aria-label={`${column.columnName} PK 설정`}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{/* NN (NOT NULL) 체크박스 */}
|
|
||||||
<div className="flex items-center justify-center pt-1">
|
|
||||||
<Checkbox
|
|
||||||
checked={column.isNullable === "NO"}
|
|
||||||
onCheckedChange={() =>
|
|
||||||
handleNullableToggle(column.columnName, column.isNullable)
|
|
||||||
}
|
|
||||||
aria-label={`${column.columnName} NOT NULL 설정`}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{/* IDX 체크박스 */}
|
|
||||||
<div className="flex items-center justify-center pt-1">
|
|
||||||
<Checkbox
|
|
||||||
checked={idxState.hasIndex}
|
|
||||||
onCheckedChange={(checked) =>
|
|
||||||
handleIndexToggle(column.columnName, "index", checked as boolean)
|
|
||||||
}
|
|
||||||
aria-label={`${column.columnName} 인덱스 설정`}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{/* UQ 체크박스 (앱 레벨 소프트 제약조건) */}
|
|
||||||
<div className="flex items-center justify-center pt-1">
|
|
||||||
<Checkbox
|
|
||||||
checked={column.isUnique === "YES"}
|
|
||||||
onCheckedChange={() =>
|
|
||||||
handleUniqueToggle(column.columnName, column.isUnique)
|
|
||||||
}
|
|
||||||
aria-label={`${column.columnName} 유니크 설정`}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
|
|
||||||
{/* 로딩 표시 */}
|
|
||||||
{columnsLoading && (
|
|
||||||
<div className="flex items-center justify-center py-4">
|
|
||||||
<LoadingSpinner />
|
|
||||||
<span className="text-muted-foreground ml-2 text-sm">더 많은 컬럼 로딩 중...</span>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 페이지 정보 (고정 하단) */}
|
|
||||||
<div className="text-muted-foreground flex-shrink-0 border-t py-2 text-center text-sm">
|
|
||||||
{columns.length} / {totalColumns} 컬럼 표시됨
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,468 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useMemo } from "react";
|
||||||
|
import { X, Type, Settings2, Tag, ToggleLeft, ChevronDown, ChevronRight } from "lucide-react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||||
|
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
|
||||||
|
import { Check, ChevronsUpDown } from "lucide-react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import type { ColumnTypeInfo, TableInfo, SecondLevelMenu } from "./types";
|
||||||
|
import { INPUT_TYPE_COLORS } from "./types";
|
||||||
|
import type { ReferenceTableColumn } from "@/lib/api/entityJoin";
|
||||||
|
import type { NumberingRuleConfig } from "@/types/numbering-rule";
|
||||||
|
|
||||||
|
export interface ColumnDetailPanelProps {
|
||||||
|
column: ColumnTypeInfo | null;
|
||||||
|
tables: TableInfo[];
|
||||||
|
referenceTableColumns: Record<string, ReferenceTableColumn[]>;
|
||||||
|
secondLevelMenus: SecondLevelMenu[];
|
||||||
|
numberingRules: NumberingRuleConfig[];
|
||||||
|
onColumnChange: (field: keyof ColumnTypeInfo, value: unknown) => void;
|
||||||
|
onClose: () => void;
|
||||||
|
onLoadReferenceColumns?: (tableName: string) => void;
|
||||||
|
/** 코드 카테고리 옵션 (value, label) */
|
||||||
|
codeCategoryOptions?: Array<{ value: string; label: string }>;
|
||||||
|
/** 참조 테이블 옵션 (value, label) */
|
||||||
|
referenceTableOptions?: Array<{ value: string; label: string }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ColumnDetailPanel({
|
||||||
|
column,
|
||||||
|
tables,
|
||||||
|
referenceTableColumns,
|
||||||
|
numberingRules,
|
||||||
|
onColumnChange,
|
||||||
|
onClose,
|
||||||
|
onLoadReferenceColumns,
|
||||||
|
codeCategoryOptions = [],
|
||||||
|
referenceTableOptions = [],
|
||||||
|
}: ColumnDetailPanelProps) {
|
||||||
|
const [advancedOpen, setAdvancedOpen] = React.useState(false);
|
||||||
|
const [entityTableOpen, setEntityTableOpen] = React.useState(false);
|
||||||
|
const [entityColumnOpen, setEntityColumnOpen] = React.useState(false);
|
||||||
|
const [numberingOpen, setNumberingOpen] = React.useState(false);
|
||||||
|
|
||||||
|
const typeConf = column ? INPUT_TYPE_COLORS[column.inputType || "text"] : null;
|
||||||
|
const refColumns = column?.referenceTable
|
||||||
|
? referenceTableColumns[column.referenceTable] ?? []
|
||||||
|
: [];
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (column?.referenceTable && column.referenceTable !== "none") {
|
||||||
|
onLoadReferenceColumns?.(column.referenceTable);
|
||||||
|
}
|
||||||
|
}, [column?.referenceTable, onLoadReferenceColumns]);
|
||||||
|
|
||||||
|
const advancedCount = useMemo(() => {
|
||||||
|
if (!column) return 0;
|
||||||
|
let n = 0;
|
||||||
|
if (column.defaultValue != null && column.defaultValue !== "") n++;
|
||||||
|
if (column.maxLength != null && column.maxLength > 0) n++;
|
||||||
|
return n;
|
||||||
|
}, [column]);
|
||||||
|
|
||||||
|
if (!column) return null;
|
||||||
|
|
||||||
|
const refTableOpts = referenceTableOptions.length
|
||||||
|
? referenceTableOptions
|
||||||
|
: [{ value: "none", label: "선택 안함" }, ...tables.map((t) => ({ value: t.tableName, label: t.displayName || t.tableName }))];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-full w-full flex-col border-l bg-card">
|
||||||
|
{/* 헤더 */}
|
||||||
|
<div className="flex flex-shrink-0 items-center justify-between border-b px-4 py-3">
|
||||||
|
<div className="flex min-w-0 items-center gap-2">
|
||||||
|
{typeConf && (
|
||||||
|
<span className={cn("rounded px-2 py-0.5 text-xs", typeConf.bgColor, typeConf.color)}>
|
||||||
|
{typeConf.label}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span className="truncate font-mono text-sm font-medium">{column.columnName}</span>
|
||||||
|
</div>
|
||||||
|
<Button type="button" variant="ghost" size="icon" className="h-8 w-8 shrink-0" onClick={onClose} aria-label="닫기">
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* [섹션 2] 타입별 상세 설정 */}
|
||||||
|
{column.inputType === "entity" && (
|
||||||
|
<section className="space-y-2">
|
||||||
|
<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}>
|
||||||
|
<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.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>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{column.inputType === "code" && (
|
||||||
|
<section className="space-y-2">
|
||||||
|
<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-2">
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label className="text-xs text-muted-foreground">공통코드 카테고리</Label>
|
||||||
|
<Select
|
||||||
|
value={column.codeCategory ?? "none"}
|
||||||
|
onValueChange={(v) => onColumnChange("codeCategory", v === "none" ? undefined : v)}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="h-9 text-xs">
|
||||||
|
<SelectValue placeholder="코드 선택" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{[{ value: "none", label: "선택 안함" }, ...codeCategoryOptions].map((opt) => (
|
||||||
|
<SelectItem key={opt.value} value={opt.value}>
|
||||||
|
{opt.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
{column.codeCategory && column.codeCategory !== "none" && (
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label className="text-xs text-muted-foreground">계층 역할</Label>
|
||||||
|
<Select
|
||||||
|
value={column.hierarchyRole ?? "none"}
|
||||||
|
onValueChange={(v) =>
|
||||||
|
onColumnChange("hierarchyRole", v === "none" ? undefined : (v as "large" | "medium" | "small"))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="h-9 text-xs">
|
||||||
|
<SelectValue placeholder="일반" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="none">일반</SelectItem>
|
||||||
|
<SelectItem value="large">대분류</SelectItem>
|
||||||
|
<SelectItem value="medium">중분류</SelectItem>
|
||||||
|
<SelectItem value="small">소분류</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{column.inputType === "category" && (
|
||||||
|
<section className="space-y-2">
|
||||||
|
<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-1.5">
|
||||||
|
<Label className="text-xs text-muted-foreground">참조 (테이블.컬럼)</Label>
|
||||||
|
<Input
|
||||||
|
value={column.categoryRef ?? ""}
|
||||||
|
onChange={(e) => onColumnChange("categoryRef", e.target.value || null)}
|
||||||
|
placeholder="테이블명.컬럼명"
|
||||||
|
className="h-9 text-xs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{column.inputType === "numbering" && (
|
||||||
|
<section className="space-y-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Settings2 className="h-4 w-4 text-muted-foreground" />
|
||||||
|
<Label className="text-sm font-medium">채번 규칙</Label>
|
||||||
|
</div>
|
||||||
|
<Popover open={numberingOpen} onOpenChange={setNumberingOpen}>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button variant="outline" className="h-9 w-full justify-between text-xs">
|
||||||
|
{column.numberingRuleId
|
||||||
|
? numberingRules.find((r) => r.ruleId === column.numberingRuleId)?.ruleName ?? column.numberingRuleId
|
||||||
|
: "규칙 선택..."}
|
||||||
|
<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("numberingRuleId", undefined);
|
||||||
|
setNumberingOpen(false);
|
||||||
|
}}
|
||||||
|
className="text-xs"
|
||||||
|
>
|
||||||
|
<Check className={cn("mr-2 h-3 w-3", !column.numberingRuleId ? "opacity-100" : "opacity-0")} />
|
||||||
|
선택 안함
|
||||||
|
</CommandItem>
|
||||||
|
{numberingRules.map((r) => (
|
||||||
|
<CommandItem
|
||||||
|
key={r.ruleId}
|
||||||
|
value={`${r.ruleName} ${r.ruleId}`}
|
||||||
|
onSelect={() => {
|
||||||
|
onColumnChange("numberingRuleId", r.ruleId);
|
||||||
|
setNumberingOpen(false);
|
||||||
|
}}
|
||||||
|
className="text-xs"
|
||||||
|
>
|
||||||
|
<Check
|
||||||
|
className={cn("mr-2 h-3 w-3", column.numberingRuleId === r.ruleId ? "opacity-100" : "opacity-0")}
|
||||||
|
/>
|
||||||
|
{r.ruleName}
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* [섹션 3] 표시 이름 */}
|
||||||
|
<section className="space-y-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Tag className="h-4 w-4 text-muted-foreground" />
|
||||||
|
<Label className="text-sm font-medium">표시 이름</Label>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
value={column.displayName ?? ""}
|
||||||
|
onChange={(e) => onColumnChange("displayName", e.target.value)}
|
||||||
|
placeholder={column.columnName}
|
||||||
|
className="h-9 text-sm"
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* [섹션 4] 표시 옵션 */}
|
||||||
|
<section className="space-y-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<ToggleLeft className="h-4 w-4 text-muted-foreground" />
|
||||||
|
<Label className="text-sm font-medium">표시 옵션</Label>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium">필수 입력</p>
|
||||||
|
<p className="text-xs text-muted-foreground">비워두면 저장할 수 없어요.</p>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={column.isNullable === "NO"}
|
||||||
|
onCheckedChange={(checked) => onColumnChange("isNullable", checked ? "NO" : "YES")}
|
||||||
|
aria-label="필수 입력"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium">읽기 전용</p>
|
||||||
|
<p className="text-xs text-muted-foreground">편집할 수 없어요.</p>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={false}
|
||||||
|
onCheckedChange={() => {}}
|
||||||
|
disabled
|
||||||
|
aria-label="읽기 전용 (향후 확장)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* [섹션 5] 고급 설정 */}
|
||||||
|
<Collapsible open={advancedOpen} onOpenChange={setAdvancedOpen}>
|
||||||
|
<CollapsibleTrigger asChild>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="flex w-full items-center justify-between py-1 text-left"
|
||||||
|
aria-expanded={advancedOpen}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{advancedOpen ? (
|
||||||
|
<ChevronDown className="h-4 w-4 text-muted-foreground" />
|
||||||
|
) : (
|
||||||
|
<ChevronRight className="h-4 w-4 text-muted-foreground" />
|
||||||
|
)}
|
||||||
|
<span className="text-sm font-medium">고급 설정</span>
|
||||||
|
{advancedCount > 0 && (
|
||||||
|
<Badge variant="secondary" className="text-xs">
|
||||||
|
{advancedCount}개 설정됨
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</CollapsibleTrigger>
|
||||||
|
<CollapsibleContent>
|
||||||
|
<div className="space-y-3 pt-2">
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label className="text-xs text-muted-foreground">기본값</Label>
|
||||||
|
<Input
|
||||||
|
value={column.defaultValue ?? ""}
|
||||||
|
onChange={(e) => onColumnChange("defaultValue", e.target.value)}
|
||||||
|
placeholder="기본값"
|
||||||
|
className="h-9 text-xs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label className="text-xs text-muted-foreground">최대 길이</Label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
value={column.maxLength ?? ""}
|
||||||
|
onChange={(e) => {
|
||||||
|
const v = e.target.value;
|
||||||
|
onColumnChange("maxLength", v === "" ? undefined : Number(v));
|
||||||
|
}}
|
||||||
|
placeholder="숫자"
|
||||||
|
className="h-9 text-xs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CollapsibleContent>
|
||||||
|
</Collapsible>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,252 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useMemo } from "react";
|
||||||
|
import { MoreHorizontal, Database, Layers, FileStack } from "lucide-react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import type { ColumnTypeInfo } from "./types";
|
||||||
|
import { INPUT_TYPE_COLORS, getColumnGroup } from "./types";
|
||||||
|
|
||||||
|
export interface ColumnGridConstraints {
|
||||||
|
primaryKey: { columns: string[] };
|
||||||
|
indexes: Array<{ columns: string[]; isUnique: boolean }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ColumnGridProps {
|
||||||
|
columns: ColumnTypeInfo[];
|
||||||
|
selectedColumn: string | null;
|
||||||
|
onSelectColumn: (columnName: string) => void;
|
||||||
|
onColumnChange: (columnName: string, field: keyof ColumnTypeInfo, value: unknown) => void;
|
||||||
|
constraints: ColumnGridConstraints;
|
||||||
|
typeFilter?: string | null;
|
||||||
|
getColumnIndexState?: (columnName: string) => { isPk: boolean; hasIndex: boolean };
|
||||||
|
}
|
||||||
|
|
||||||
|
function getIndexState(
|
||||||
|
columnName: string,
|
||||||
|
constraints: ColumnGridConstraints,
|
||||||
|
): { isPk: boolean; hasIndex: boolean } {
|
||||||
|
const isPk = constraints.primaryKey.columns.includes(columnName);
|
||||||
|
const hasIndex = constraints.indexes.some(
|
||||||
|
(idx) => !idx.isUnique && idx.columns.length === 1 && idx.columns[0] === columnName,
|
||||||
|
);
|
||||||
|
return { isPk, hasIndex };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 그룹 헤더 라벨 */
|
||||||
|
const GROUP_LABELS: Record<string, { icon: React.ElementType; label: string }> = {
|
||||||
|
basic: { icon: FileStack, label: "기본 정보" },
|
||||||
|
reference: { icon: Layers, label: "참조 정보" },
|
||||||
|
meta: { icon: Database, label: "메타 정보" },
|
||||||
|
};
|
||||||
|
|
||||||
|
export function ColumnGrid({
|
||||||
|
columns,
|
||||||
|
selectedColumn,
|
||||||
|
onSelectColumn,
|
||||||
|
constraints,
|
||||||
|
typeFilter = null,
|
||||||
|
getColumnIndexState: externalGetIndexState,
|
||||||
|
}: ColumnGridProps) {
|
||||||
|
const getIdxState = useMemo(
|
||||||
|
() => externalGetIndexState ?? ((name: string) => getIndexState(name, constraints)),
|
||||||
|
[constraints, externalGetIndexState],
|
||||||
|
);
|
||||||
|
|
||||||
|
/** typeFilter 적용 후 그룹별로 정렬 */
|
||||||
|
const filteredAndGrouped = useMemo(() => {
|
||||||
|
const filtered =
|
||||||
|
typeFilter != null ? columns.filter((c) => (c.inputType || "text") === typeFilter) : columns;
|
||||||
|
const groups = { basic: [] as ColumnTypeInfo[], reference: [] as ColumnTypeInfo[], meta: [] as ColumnTypeInfo[] };
|
||||||
|
for (const col of filtered) {
|
||||||
|
const group = getColumnGroup(col);
|
||||||
|
groups[group].push(col);
|
||||||
|
}
|
||||||
|
return groups;
|
||||||
|
}, [columns, typeFilter]);
|
||||||
|
|
||||||
|
const totalFiltered =
|
||||||
|
filteredAndGrouped.basic.length + filteredAndGrouped.reference.length + filteredAndGrouped.meta.length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-1 flex-col overflow-hidden">
|
||||||
|
<div
|
||||||
|
className="grid flex-shrink-0 items-center border-b bg-muted/50 px-4 py-2 text-xs font-semibold text-foreground"
|
||||||
|
style={{ gridTemplateColumns: "4px 140px 1fr 100px 160px 40px" }}
|
||||||
|
>
|
||||||
|
<span />
|
||||||
|
<span>라벨 · 컬럼명</span>
|
||||||
|
<span>참조/설정</span>
|
||||||
|
<span>타입</span>
|
||||||
|
<span className="text-center">PK / NN / IDX / UQ</span>
|
||||||
|
<span />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 overflow-y-auto">
|
||||||
|
{totalFiltered === 0 ? (
|
||||||
|
<div className="flex items-center justify-center py-12 text-sm text-muted-foreground">
|
||||||
|
{typeFilter ? "해당 타입의 컬럼이 없습니다." : "컬럼이 없습니다."}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
(["basic", "reference", "meta"] as const).map((groupKey) => {
|
||||||
|
const list = filteredAndGrouped[groupKey];
|
||||||
|
if (list.length === 0) return null;
|
||||||
|
const { icon: Icon, label } = GROUP_LABELS[groupKey];
|
||||||
|
return (
|
||||||
|
<div key={groupKey} className="space-y-1 py-2">
|
||||||
|
<div className="flex items-center gap-2 border-b border-border/60 px-4 pb-1.5">
|
||||||
|
<Icon className="h-4 w-4 text-muted-foreground" />
|
||||||
|
<span className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
<Badge variant="secondary" className="text-xs">
|
||||||
|
{list.length}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
{list.map((column) => {
|
||||||
|
const typeConf = INPUT_TYPE_COLORS[column.inputType || "text"] || INPUT_TYPE_COLORS.text;
|
||||||
|
const idxState = getIdxState(column.columnName);
|
||||||
|
const isSelected = selectedColumn === column.columnName;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={column.columnName}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={() => onSelectColumn(column.columnName)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter" || e.key === " ") {
|
||||||
|
e.preventDefault();
|
||||||
|
onSelectColumn(column.columnName);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className={cn(
|
||||||
|
"grid min-h-12 cursor-pointer items-center gap-2 rounded-md border px-4 py-2 transition-colors",
|
||||||
|
"grid-cols-[4px_140px_1fr_100px_160px_40px]",
|
||||||
|
"bg-card border-transparent hover:border-border hover:shadow-sm",
|
||||||
|
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)} />
|
||||||
|
|
||||||
|
{/* 라벨 + 컬럼명 */}
|
||||||
|
<div className="min-w-0">
|
||||||
|
<div className="truncate text-sm font-medium">
|
||||||
|
{column.displayName || column.columnName}
|
||||||
|
</div>
|
||||||
|
<div className="truncate font-mono text-xs text-muted-foreground">
|
||||||
|
{column.columnName}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 참조/설정 칩 */}
|
||||||
|
<div className="flex min-w-0 flex-wrap gap-1">
|
||||||
|
{column.inputType === "entity" && column.referenceTable && column.referenceTable !== "none" && (
|
||||||
|
<>
|
||||||
|
<Badge variant="outline" className="text-xs font-normal">
|
||||||
|
{column.referenceTable}
|
||||||
|
</Badge>
|
||||||
|
<span className="text-muted-foreground text-xs">→</span>
|
||||||
|
<Badge variant="outline" className="text-xs font-normal">
|
||||||
|
{column.referenceColumn || "—"}
|
||||||
|
</Badge>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{column.inputType === "code" && (
|
||||||
|
<span className="text-muted-foreground truncate text-xs">
|
||||||
|
{column.codeCategory ?? "—"} · {column.defaultValue ?? ""}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{column.inputType === "numbering" && column.numberingRuleId && (
|
||||||
|
<Badge variant="outline" className="text-xs font-normal">
|
||||||
|
{column.numberingRuleId}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{column.inputType !== "entity" &&
|
||||||
|
column.inputType !== "code" &&
|
||||||
|
column.inputType !== "numbering" &&
|
||||||
|
(column.defaultValue ? (
|
||||||
|
<span className="text-muted-foreground truncate text-xs">{column.defaultValue}</span>
|
||||||
|
) : (
|
||||||
|
<span className="text-muted-foreground/60 text-xs">—</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 타입 뱃지 */}
|
||||||
|
<div className={cn("rounded-md border px-2 py-0.5 text-xs", typeConf.bgColor, typeConf.color)}>
|
||||||
|
<span className="mr-1 inline-block h-1.5 w-1.5 rounded-full bg-current opacity-70" />
|
||||||
|
{typeConf.label}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* PK / NN / IDX / UQ (읽기 전용) */}
|
||||||
|
<div className="flex flex-wrap items-center justify-center gap-1">
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"rounded border px-1.5 py-0.5 text-[10px] font-medium",
|
||||||
|
idxState.isPk
|
||||||
|
? "border-primary/30 bg-primary/10 text-primary"
|
||||||
|
: "border-border bg-muted/50 text-muted-foreground",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
PK
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"rounded border px-1.5 py-0.5 text-[10px] font-medium",
|
||||||
|
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",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
NN
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"rounded border px-1.5 py-0.5 text-[10px] font-medium",
|
||||||
|
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",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
IDX
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"rounded border px-1.5 py-0.5 text-[10px] font-medium",
|
||||||
|
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",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
UQ
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-center">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-8 w-8"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onSelectColumn(column.columnName);
|
||||||
|
}}
|
||||||
|
aria-label="상세 설정"
|
||||||
|
>
|
||||||
|
<MoreHorizontal className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,114 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useMemo } from "react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import type { ColumnTypeInfo } from "./types";
|
||||||
|
import { INPUT_TYPE_COLORS } from "./types";
|
||||||
|
|
||||||
|
export interface TypeOverviewStripProps {
|
||||||
|
columns: ColumnTypeInfo[];
|
||||||
|
activeFilter?: string | null;
|
||||||
|
onFilterChange?: (type: string | null) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** inputType별 카운트 계산 */
|
||||||
|
function countByInputType(columns: ColumnTypeInfo[]): Record<string, number> {
|
||||||
|
const counts: Record<string, number> = {};
|
||||||
|
for (const col of columns) {
|
||||||
|
const t = col.inputType || "text";
|
||||||
|
counts[t] = (counts[t] || 0) + 1;
|
||||||
|
}
|
||||||
|
return counts;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 도넛 차트용 비율 (0~1) 배열 및 라벨 순서 */
|
||||||
|
function getDonutSegments(counts: Record<string, number>, total: number): Array<{ type: string; ratio: number }> {
|
||||||
|
const order = Object.keys(INPUT_TYPE_COLORS);
|
||||||
|
return order
|
||||||
|
.filter((type) => (counts[type] || 0) > 0)
|
||||||
|
.map((type) => ({ type, ratio: (counts[type] || 0) / total }));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TypeOverviewStrip({
|
||||||
|
columns,
|
||||||
|
activeFilter = null,
|
||||||
|
onFilterChange,
|
||||||
|
}: TypeOverviewStripProps) {
|
||||||
|
const { counts, total, segments } = useMemo(() => {
|
||||||
|
const counts = countByInputType(columns);
|
||||||
|
const total = columns.length || 1;
|
||||||
|
const segments = getDonutSegments(counts, total);
|
||||||
|
return { counts, total, segments };
|
||||||
|
}, [columns]);
|
||||||
|
|
||||||
|
/** stroke-dasharray: 비율만큼 둘레에 할당 (둘레 100 기준) */
|
||||||
|
const circumference = 100;
|
||||||
|
let offset = 0;
|
||||||
|
const segmentPaths = segments.map(({ type, ratio }) => {
|
||||||
|
const length = ratio * circumference;
|
||||||
|
const dashArray = `${length} ${circumference - length}`;
|
||||||
|
const dashOffset = -offset;
|
||||||
|
offset += length;
|
||||||
|
const conf = INPUT_TYPE_COLORS[type] || { color: "text-muted-foreground", bgColor: "bg-muted" };
|
||||||
|
return {
|
||||||
|
type,
|
||||||
|
dashArray,
|
||||||
|
dashOffset,
|
||||||
|
...conf,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-shrink-0 items-center gap-3 border-b bg-muted/30 px-5 py-2.5">
|
||||||
|
{/* SVG 도넛 (원형 stroke) */}
|
||||||
|
<div className="flex h-10 w-10 flex-shrink-0 items-center justify-center">
|
||||||
|
<svg className="h-10 w-10 -rotate-90" viewBox="0 0 36 36">
|
||||||
|
{segmentPaths.map((seg) => (
|
||||||
|
<g key={seg.type} className={cn(seg.color, "opacity-80")}>
|
||||||
|
<circle
|
||||||
|
cx="18"
|
||||||
|
cy="18"
|
||||||
|
r="14"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="6"
|
||||||
|
strokeDasharray={seg.dashArray}
|
||||||
|
strokeDashoffset={seg.dashOffset}
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
))}
|
||||||
|
{segments.length === 0 && (
|
||||||
|
<circle cx="18" cy="18" r="14" fill="none" stroke="currentColor" strokeWidth="6" className="text-muted-foreground/50" />
|
||||||
|
)}
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 타입 칩 목록 (클릭 시 필터 토글) */}
|
||||||
|
<div className="flex min-w-0 flex-1 flex-wrap items-center gap-2">
|
||||||
|
{Object.entries(counts)
|
||||||
|
.sort((a, b) => (b[1] ?? 0) - (a[1] ?? 0))
|
||||||
|
.map(([type]) => {
|
||||||
|
const conf = INPUT_TYPE_COLORS[type] || { color: "text-muted-foreground", bgColor: "bg-muted", label: type };
|
||||||
|
const isActive = activeFilter === null || activeFilter === type;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={type}
|
||||||
|
type="button"
|
||||||
|
onClick={() => onFilterChange?.(activeFilter === type ? null : type)}
|
||||||
|
className={cn(
|
||||||
|
"rounded-md border px-2 py-1 text-xs font-medium transition-colors",
|
||||||
|
conf.bgColor,
|
||||||
|
conf.color,
|
||||||
|
"border-current/20",
|
||||||
|
isActive ? "ring-1 ring-ring" : "opacity-70 hover:opacity-100",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{conf.label} {counts[type]}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
/**
|
||||||
|
* 테이블 타입 관리 페이지 공통 타입
|
||||||
|
* page.tsx에서 추출한 인터페이스 및 타입별 색상/그룹 유틸
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface TableInfo {
|
||||||
|
tableName: string;
|
||||||
|
displayName: string;
|
||||||
|
description: string;
|
||||||
|
columnCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ColumnTypeInfo {
|
||||||
|
columnName: string;
|
||||||
|
displayName: string;
|
||||||
|
inputType: string;
|
||||||
|
detailSettings: string;
|
||||||
|
description: string;
|
||||||
|
isNullable: string;
|
||||||
|
isUnique: string;
|
||||||
|
defaultValue?: string;
|
||||||
|
maxLength?: number;
|
||||||
|
numericPrecision?: number;
|
||||||
|
numericScale?: number;
|
||||||
|
codeCategory?: string;
|
||||||
|
codeValue?: string;
|
||||||
|
referenceTable?: string;
|
||||||
|
referenceColumn?: string;
|
||||||
|
displayColumn?: string;
|
||||||
|
categoryMenus?: number[];
|
||||||
|
hierarchyRole?: "large" | "medium" | "small";
|
||||||
|
numberingRuleId?: string;
|
||||||
|
categoryRef?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SecondLevelMenu {
|
||||||
|
menuObjid: number;
|
||||||
|
menuName: string;
|
||||||
|
parentMenuName: string;
|
||||||
|
screenCode?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 컬럼 그룹 분류 */
|
||||||
|
export type ColumnGroup = "basic" | "reference" | "meta";
|
||||||
|
|
||||||
|
/** 타입별 색상 매핑 (다크모드 호환 레이어 사용) */
|
||||||
|
export interface TypeColorConfig {
|
||||||
|
color: string;
|
||||||
|
bgColor: string;
|
||||||
|
label: string;
|
||||||
|
icon?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 입력 타입별 색상 맵 - 배경/텍스트/보더는 다크에서 자동 변환 */
|
||||||
|
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: "라디오" },
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 컬럼 그룹 판별 */
|
||||||
|
export function getColumnGroup(col: ColumnTypeInfo): ColumnGroup {
|
||||||
|
const metaCols = ["id", "created_date", "updated_date", "writer", "company_code"];
|
||||||
|
if (metaCols.includes(col.columnName)) return "meta";
|
||||||
|
if (["entity", "code", "category"].includes(col.inputType)) return "reference";
|
||||||
|
return "basic";
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue