diff --git a/frontend/app/(main)/admin/systemMng/tableMngList/page.tsx b/frontend/app/(main)/admin/systemMng/tableMngList/page.tsx index 3064e4e5..79a57134 100644 --- a/frontend/app/(main)/admin/systemMng/tableMngList/page.tsx +++ b/frontend/app/(main)/admin/systemMng/tableMngList/page.tsx @@ -50,43 +50,10 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog"; - -interface TableInfo { - tableName: string; - displayName: string; - 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; -} +import type { TableInfo, ColumnTypeInfo, SecondLevelMenu } from "@/components/admin/table-type/types"; +import { TypeOverviewStrip } from "@/components/admin/table-type/TypeOverviewStrip"; +import { ColumnGrid } from "@/components/admin/table-type/ColumnGrid"; +import { ColumnDetailPanel } from "@/components/admin/table-type/ColumnDetailPanel"; export default function TableManagementPage() { const { userLang, getText } = useMultiLang({ companyCode: "*" }); @@ -164,6 +131,11 @@ export default function TableManagementPage() { // ์„ ํƒ๋œ ํ…Œ์ด๋ธ” ๋ชฉ๋ก (์ฒดํฌ๋ฐ•์Šค) const [selectedTableIds, setSelectedTableIds] = useState>(new Set()); + // ์ปฌ๋Ÿผ ๊ทธ๋ฆฌ๋“œ: ์„ ํƒ๋œ ์ปฌ๋Ÿผ(์šฐ์ธก ์ƒ์„ธ ํŒจ๋„ ํ‘œ์‹œ) + const [selectedColumn, setSelectedColumn] = useState(null); + // ํƒ€์ž… ์˜ค๋ฒ„๋ทฐ ์ŠคํŠธ๋ฆฝ: ํƒ€์ž… ํ•„ํ„ฐ (null = ์ „์ฒด) + const [typeFilter, setTypeFilter] = useState(null); + // ์ตœ๊ณ  ๊ด€๋ฆฌ์ž ์—ฌ๋ถ€ ํ™•์ธ (ํšŒ์‚ฌ์ฝ”๋“œ๊ฐ€ "*" AND userType์ด "SUPER_ADMIN") const isSuperAdmin = user?.companyCode === "*" && user?.userType === "SUPER_ADMIN"; @@ -442,6 +414,8 @@ export default function TableManagementPage() { setSelectedTable(tableName); setCurrentPage(1); setColumns([]); + setSelectedColumn(null); + setTypeFilter(null); // ์„ ํƒ๋œ ํ…Œ์ด๋ธ” ์ •๋ณด์—์„œ ๋ผ๋ฒจ ์„ค์ • const tableInfo = tables.find((table) => table.tableName === tableName); @@ -1588,417 +1562,56 @@ export default function TableManagementPage() { {getTextFromUI(TABLE_MANAGEMENT_KEYS.MESSAGE_NO_COLUMNS, "์ปฌ๋Ÿผ์ด ์—†์Šต๋‹ˆ๋‹ค")} ) : ( -
- {/* ์ปฌ๋Ÿผ ํ—ค๋” (๊ณ ์ •) */} -
-
๋ผ๋ฒจ
-
์ปฌ๋Ÿผ๋ช…
-
์ž…๋ ฅ ํƒ€์ž…
-
์„ค๋ช…
-
Primary
-
NotNull
-
Index
-
Unique
-
- - {/* ์ปฌ๋Ÿผ ๋ฆฌ์ŠคํŠธ (์Šคํฌ๋กค ์˜์—ญ) */} -
{ - const { scrollTop, scrollHeight, clientHeight } = e.currentTarget; - // ์Šคํฌ๋กค์ด ๋์— ๊ฐ€๊นŒ์›Œ์ง€๋ฉด ๋” ๋งŽ์€ ๋ฐ์ดํ„ฐ ๋กœ๋“œ - if (scrollHeight - scrollTop <= clientHeight + 100) { - loadMoreColumns(); - } - }} - > - {columns.map((column, index) => { - const idxState = getColumnIndexState(column.columnName); - return ( -
-
- handleLabelChange(column.columnName, e.target.value)} - placeholder={column.columnName} - className="h-8 text-xs" - /> -
-
-
{column.columnName}
-
-
-
- {/* ์ž…๋ ฅ ํƒ€์ž… ์„ ํƒ */} - - {/* ์ž…๋ ฅ ํƒ€์ž…์ด 'code'์ธ ๊ฒฝ์šฐ ๊ณตํ†ต์ฝ”๋“œ ์„ ํƒ */} - {column.inputType === "code" && ( - <> - - {/* ๊ณ„์ธต๊ตฌ์กฐ ์—ญํ•  ์„ ํƒ */} - {column.codeCategory && column.codeCategory !== "none" && ( - - )} - - )} - {/* ์นดํ…Œ๊ณ ๋ฆฌ ํƒ€์ž…: ์ฐธ์กฐ ์„ค์ • */} - {column.inputType === "category" && ( -
- - { - const val = e.target.value || null; - setColumns((prev) => - prev.map((c) => - c.columnName === column.columnName - ? { ...c, categoryRef: val } - : c - ) - ); - }} - placeholder="ํ…Œ์ด๋ธ”๋ช….์ปฌ๋Ÿผ๋ช…" - className="h-8 text-xs" - /> -

- ๋‹ค๋ฅธ ํ…Œ์ด๋ธ”์˜ ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ์ฐธ์กฐ ์‹œ ์ž…๋ ฅ -

-
- )} - {/* ์ž…๋ ฅ ํƒ€์ž…์ด 'entity'์ธ ๊ฒฝ์šฐ ์ฐธ์กฐ ํ…Œ์ด๋ธ” ์„ ํƒ */} - {column.inputType === "entity" && ( - <> - {/* ์ฐธ์กฐ ํ…Œ์ด๋ธ” - ๊ฒ€์ƒ‰ ๊ฐ€๋Šฅํ•œ Combobox */} -
- - - setEntityComboboxOpen((prev) => ({ - ...prev, - [column.columnName]: { ...prev[column.columnName], table: open }, - })) - } - > - - - - - - - - - ํ…Œ์ด๋ธ”์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. - - - {referenceTableOptions.map((option) => ( - { - handleDetailSettingsChange( - column.columnName, - "entity", - option.value, - ); - setEntityComboboxOpen((prev) => ({ - ...prev, - [column.columnName]: { - ...prev[column.columnName], - table: false, - }, - })); - }} - className="text-xs" - > - -
- {option.label} - {option.value !== "none" && ( - - {option.value} - - )} -
-
- ))} -
-
-
-
-
-
- - {/* ์กฐ์ธ ์ปฌ๋Ÿผ - ๊ฒ€์ƒ‰ ๊ฐ€๋Šฅํ•œ Combobox */} - {column.referenceTable && column.referenceTable !== "none" && ( -
- - - setEntityComboboxOpen((prev) => ({ - ...prev, - [column.columnName]: { ...prev[column.columnName], joinColumn: open }, - })) - } - > - - - - - - - - - ์ปฌ๋Ÿผ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. - - - { - handleDetailSettingsChange( - column.columnName, - "entity_reference_column", - "none", - ); - setEntityComboboxOpen((prev) => ({ - ...prev, - [column.columnName]: { - ...prev[column.columnName], - joinColumn: false, - }, - })); - }} - className="text-xs" - > - - -- ์„ ํƒ ์•ˆํ•จ -- - - {referenceTableColumns[column.referenceTable]?.map((refCol) => ( - { - handleDetailSettingsChange( - column.columnName, - "entity_reference_column", - refCol.columnName, - ); - setEntityComboboxOpen((prev) => ({ - ...prev, - [column.columnName]: { - ...prev[column.columnName], - joinColumn: false, - }, - })); - }} - className="text-xs" - > - -
- {refCol.columnName} - {refCol.displayName && ( - - {refCol.displayName} - - )} -
-
- ))} -
-
-
-
-
-
- )} - - {/* ์„ค์ • ์™„๋ฃŒ ํ‘œ์‹œ */} - {column.referenceTable && - column.referenceTable !== "none" && - column.referenceColumn && - column.referenceColumn !== "none" && ( -
- - ์„ค์ • ์™„๋ฃŒ -
- )} - - )} - {/* ์ฑ„๋ฒˆ ํƒ€์ž…์€ ์˜ต์…˜์„ค์ • > ์ฑ„๋ฒˆ์„ค์ •์—์„œ ๊ด€๋ฆฌ (๋ณ„๋„ ์„ ํƒ ๋ถˆํ•„์š”) */} -
-
-
- handleColumnChange(index, "description", e.target.value)} - placeholder="์„ค๋ช…" - className="h-8 w-full text-xs" - /> -
- {/* PK ์ฒดํฌ๋ฐ•์Šค */} -
- - handlePkToggle(column.columnName, checked as boolean) - } - aria-label={`${column.columnName} PK ์„ค์ •`} - /> -
- {/* NN (NOT NULL) ์ฒดํฌ๋ฐ•์Šค */} -
- - handleNullableToggle(column.columnName, column.isNullable) - } - aria-label={`${column.columnName} NOT NULL ์„ค์ •`} - /> -
- {/* IDX ์ฒดํฌ๋ฐ•์Šค */} -
- - handleIndexToggle(column.columnName, "index", checked as boolean) - } - aria-label={`${column.columnName} ์ธ๋ฑ์Šค ์„ค์ •`} - /> -
- {/* UQ ์ฒดํฌ๋ฐ•์Šค (์•ฑ ๋ ˆ๋ฒจ ์†Œํ”„ํŠธ ์ œ์•ฝ์กฐ๊ฑด) */} -
- - handleUniqueToggle(column.columnName, column.isUnique) - } - aria-label={`${column.columnName} ์œ ๋‹ˆํฌ ์„ค์ •`} - /> -
-
- ); - })} - - {/* ๋กœ๋”ฉ ํ‘œ์‹œ */} - {columnsLoading && ( -
- - ๋” ๋งŽ์€ ์ปฌ๋Ÿผ ๋กœ๋”ฉ ์ค‘... -
- )} -
- - {/* ํŽ˜์ด์ง€ ์ •๋ณด (๊ณ ์ • ํ•˜๋‹จ) */} -
- {columns.length} / {totalColumns} ์ปฌ๋Ÿผ ํ‘œ์‹œ๋จ +
+
+ + { + const idx = columns.findIndex((c) => c.columnName === columnName); + if (idx >= 0) handleColumnChange(idx, field, value); + }} + constraints={constraints} + typeFilter={typeFilter} + getColumnIndexState={getColumnIndexState} + />
+ {selectedColumn && ( +
+ 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} + /> +
+ )}
)} diff --git a/frontend/components/admin/table-type/ColumnDetailPanel.tsx b/frontend/components/admin/table-type/ColumnDetailPanel.tsx new file mode 100644 index 00000000..39914dbe --- /dev/null +++ b/frontend/components/admin/table-type/ColumnDetailPanel.tsx @@ -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; + 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 ( +
+ {/* ํ—ค๋” */} +
+
+ {typeConf && ( + + {typeConf.label} + + )} + {column.columnName} +
+ +
+ +
+ {/* [์„น์…˜ 1] ๋ฐ์ดํ„ฐ ํƒ€์ž… ์„ ํƒ */} +
+
+ + +
+
+ {Object.entries(INPUT_TYPE_COLORS).map(([type, conf]) => ( + + ))} +
+
+ + {/* [์„น์…˜ 2] ํƒ€์ž…๋ณ„ ์ƒ์„ธ ์„ค์ • */} + {column.inputType === "entity" && ( +
+
+ + +
+
+
+ + + + + + + + + + ํ…Œ์ด๋ธ”์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. + + {refTableOpts.map((opt) => ( + { + onColumnChange("referenceTable", opt.value === "none" ? undefined : opt.value); + if (opt.value !== "none") onLoadReferenceColumns?.(opt.value); + setEntityTableOpen(false); + }} + className="text-xs" + > + + {opt.label} + + ))} + + + + + +
+ {column.referenceTable && column.referenceTable !== "none" && ( +
+ + + + + + + + + + ์ปฌ๋Ÿผ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. + + { + onColumnChange("referenceColumn", undefined); + setEntityColumnOpen(false); + }} + className="text-xs" + > + + ์„ ํƒ ์•ˆํ•จ + + {refColumns.map((refCol) => ( + { + onColumnChange("referenceColumn", refCol.columnName); + setEntityColumnOpen(false); + }} + className="text-xs" + > + + {refCol.columnName} + + ))} + + + + + +
+ )} +
+
+ )} + + {column.inputType === "code" && ( +
+
+ + +
+
+
+ + +
+ {column.codeCategory && column.codeCategory !== "none" && ( +
+ + +
+ )} +
+
+ )} + + {column.inputType === "category" && ( +
+
+ + +
+
+ + onColumnChange("categoryRef", e.target.value || null)} + placeholder="ํ…Œ์ด๋ธ”๋ช….์ปฌ๋Ÿผ๋ช…" + className="h-9 text-xs" + /> +
+
+ )} + + {column.inputType === "numbering" && ( +
+
+ + +
+ + + + + + + + + ๊ทœ์น™์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. + + { + onColumnChange("numberingRuleId", undefined); + setNumberingOpen(false); + }} + className="text-xs" + > + + ์„ ํƒ ์•ˆํ•จ + + {numberingRules.map((r) => ( + { + onColumnChange("numberingRuleId", r.ruleId); + setNumberingOpen(false); + }} + className="text-xs" + > + + {r.ruleName} + + ))} + + + + + +
+ )} + + {/* [์„น์…˜ 3] ํ‘œ์‹œ ์ด๋ฆ„ */} +
+
+ + +
+ onColumnChange("displayName", e.target.value)} + placeholder={column.columnName} + className="h-9 text-sm" + /> +
+ + {/* [์„น์…˜ 4] ํ‘œ์‹œ ์˜ต์…˜ */} +
+
+ + +
+
+
+
+

ํ•„์ˆ˜ ์ž…๋ ฅ

+

๋น„์›Œ๋‘๋ฉด ์ €์žฅํ•  ์ˆ˜ ์—†์–ด์š”.

+
+ onColumnChange("isNullable", checked ? "NO" : "YES")} + aria-label="ํ•„์ˆ˜ ์ž…๋ ฅ" + /> +
+
+
+

์ฝ๊ธฐ ์ „์šฉ

+

ํŽธ์ง‘ํ•  ์ˆ˜ ์—†์–ด์š”.

+
+ {}} + disabled + aria-label="์ฝ๊ธฐ ์ „์šฉ (ํ–ฅํ›„ ํ™•์žฅ)" + /> +
+
+
+ + {/* [์„น์…˜ 5] ๊ณ ๊ธ‰ ์„ค์ • */} + + + + + +
+
+ + onColumnChange("defaultValue", e.target.value)} + placeholder="๊ธฐ๋ณธ๊ฐ’" + className="h-9 text-xs" + /> +
+
+ + { + const v = e.target.value; + onColumnChange("maxLength", v === "" ? undefined : Number(v)); + }} + placeholder="์ˆซ์ž" + className="h-9 text-xs" + /> +
+
+
+
+
+
+ ); +} diff --git a/frontend/components/admin/table-type/ColumnGrid.tsx b/frontend/components/admin/table-type/ColumnGrid.tsx new file mode 100644 index 00000000..e520fc43 --- /dev/null +++ b/frontend/components/admin/table-type/ColumnGrid.tsx @@ -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 = { + 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 ( +
+
+ + ๋ผ๋ฒจ ยท ์ปฌ๋Ÿผ๋ช… + ์ฐธ์กฐ/์„ค์ • + ํƒ€์ž… + PK / NN / IDX / UQ + +
+ +
+ {totalFiltered === 0 ? ( +
+ {typeFilter ? "ํ•ด๋‹น ํƒ€์ž…์˜ ์ปฌ๋Ÿผ์ด ์—†์Šต๋‹ˆ๋‹ค." : "์ปฌ๋Ÿผ์ด ์—†์Šต๋‹ˆ๋‹ค."} +
+ ) : ( + (["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 ( +
+
+ + + {label} + + + {list.length} + +
+ {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 ( +
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 ์ƒ‰์ƒ๋ฐ” */} +
+ + {/* ๋ผ๋ฒจ + ์ปฌ๋Ÿผ๋ช… */} +
+
+ {column.displayName || column.columnName} +
+
+ {column.columnName} +
+
+ + {/* ์ฐธ์กฐ/์„ค์ • ์นฉ */} +
+ {column.inputType === "entity" && column.referenceTable && column.referenceTable !== "none" && ( + <> + + {column.referenceTable} + + โ†’ + + {column.referenceColumn || "โ€”"} + + + )} + {column.inputType === "code" && ( + + {column.codeCategory ?? "โ€”"} ยท {column.defaultValue ?? ""} + + )} + {column.inputType === "numbering" && column.numberingRuleId && ( + + {column.numberingRuleId} + + )} + {column.inputType !== "entity" && + column.inputType !== "code" && + column.inputType !== "numbering" && + (column.defaultValue ? ( + {column.defaultValue} + ) : ( + โ€” + ))} +
+ + {/* ํƒ€์ž… ๋ฑƒ์ง€ */} +
+ + {typeConf.label} +
+ + {/* PK / NN / IDX / UQ (์ฝ๊ธฐ ์ „์šฉ) */} +
+ + PK + + + NN + + + IDX + + + UQ + +
+ +
+ +
+
+ ); + })} +
+ ); + }) + )} +
+
+ ); +} diff --git a/frontend/components/admin/table-type/TypeOverviewStrip.tsx b/frontend/components/admin/table-type/TypeOverviewStrip.tsx new file mode 100644 index 00000000..bdb27f47 --- /dev/null +++ b/frontend/components/admin/table-type/TypeOverviewStrip.tsx @@ -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 { + const counts: Record = {}; + for (const col of columns) { + const t = col.inputType || "text"; + counts[t] = (counts[t] || 0) + 1; + } + return counts; +} + +/** ๋„๋„› ์ฐจํŠธ์šฉ ๋น„์œจ (0~1) ๋ฐฐ์—ด ๋ฐ ๋ผ๋ฒจ ์ˆœ์„œ */ +function getDonutSegments(counts: Record, 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 ( +
+ {/* SVG ๋„๋„› (์›ํ˜• stroke) */} +
+ + {segmentPaths.map((seg) => ( + + + + ))} + {segments.length === 0 && ( + + )} + +
+ + {/* ํƒ€์ž… ์นฉ ๋ชฉ๋ก (ํด๋ฆญ ์‹œ ํ•„ํ„ฐ ํ† ๊ธ€) */} +
+ {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 ( + + ); + })} +
+
+ ); +} diff --git a/frontend/components/admin/table-type/types.ts b/frontend/components/admin/table-type/types.ts new file mode 100644 index 00000000..8adbcb62 --- /dev/null +++ b/frontend/components/admin/table-type/types.ts @@ -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 = { + 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"; +}