From 43aafb36c186c6b53f00e0c8f6a83046f79d6bb6 Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Mon, 16 Mar 2026 17:58:37 +0900 Subject: [PATCH] feat: enhance table management page with improved filtering and UI updates - Implemented Korean prioritization in table filtering, allowing for better sorting of table names based on Korean characters. - Updated the UI to a more compact design with a top bar for better accessibility and user experience. - Added new button styles and functionalities for creating and duplicating tables, enhancing the overall management capabilities. - Improved the column detail panel with clearer labeling and enhanced interaction for selecting data types and reference tables. These changes aim to streamline the table management process and improve usability within the ERP system. --- .../admin/systemMng/tableMngList/page.tsx | 663 +++++++++--------- .../admin/table-type/ColumnDetailPanel.tsx | 230 +++--- .../admin/table-type/ColumnGrid.tsx | 71 +- frontend/components/admin/table-type/types.ts | 28 +- 4 files changed, 527 insertions(+), 465 deletions(-) diff --git a/frontend/app/(main)/admin/systemMng/tableMngList/page.tsx b/frontend/app/(main)/admin/systemMng/tableMngList/page.tsx index 79a57134..29886bbd 100644 --- a/frontend/app/(main)/admin/systemMng/tableMngList/page.tsx +++ b/frontend/app/(main)/admin/systemMng/tableMngList/page.tsx @@ -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 ( -
-
- {/* 페이지 헤더 */} -
-
-
-

- {getTextFromUI(TABLE_MANAGEMENT_KEYS.PAGE_TITLE, "테이블 타입 관리")} -

-

- {getTextFromUI( - TABLE_MANAGEMENT_KEYS.PAGE_DESCRIPTION, - "데이터베이스 테이블과 컬럼의 타입을 관리합니다", - )} -

- {isSuperAdmin && ( -

- 최고 관리자 권한으로 새 테이블 생성 및 컬럼 추가가 가능합니다 -

- )} -
- -
- {/* DDL 기능 버튼들 (최고 관리자만) */} - {isSuperAdmin && ( - <> - - - - - {selectedTable && ( - - )} - - - - )} - +
+ {/* 컴팩트 탑바 (52px) */} +
+
+ +

+ {getTextFromUI(TABLE_MANAGEMENT_KEYS.PAGE_TITLE, "테이블 타입 관리")} +

+ + {tables.length} 테이블 + +
+
+ {isSuperAdmin && ( + <> + + {selectedTable && ( + + )} + + + )} + +
+
+ + {/* 3패널 메인 */} +
+ {/* 좌측: 테이블 목록 (240px) */} +
+ {/* 검색 */} +
+
+ + setSearchTerm(e.target.value)} + className="bg-background h-[34px] pl-8 text-xs" + />
+ {isSuperAdmin && ( +
+
+ 0 && + filteredTables.every((table) => selectedTableIds.has(table.tableName)) + } + onCheckedChange={handleSelectAll} + aria-label="전체 선택" + className="h-3.5 w-3.5" + /> + + {selectedTableIds.size > 0 ? `${selectedTableIds.size}개` : "전체"} + +
+ {selectedTableIds.size > 0 && ( + + )} +
+ )} +
+ + {/* 테이블 리스트 */} +
+ {loading ? ( +
+ +
+ ) : filteredTables.length === 0 ? ( +
+ {getTextFromUI(TABLE_MANAGEMENT_KEYS.MESSAGE_NO_TABLES, "테이블이 없습니다")} +
+ ) : ( + 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 ( +
+ {showDivider && ( +
+ {isKo ? "한글" : "ENGLISH"} +
+ )} +
handleTableSelect(table.tableName)} + role="button" + tabIndex={0} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + handleTableSelect(table.tableName); + } + }} + > + {isActive && ( +
+ )} + {isSuperAdmin && ( + 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()} + /> + )} +
+
+ + {table.displayName || table.tableName} + +
+
+ {table.tableName} +
+
+ + {table.columnCount} + +
+
+ ); + }) + )} +
+ + {/* 하단 정보 */} +
+ {filteredTables.length} / {tables.length} 테이블
- - {/* 검색 */} -
-
- + {/* 중앙: 컬럼 그리드 */} +
+ {!selectedTable ? ( +
+ +

+ {getTextFromUI(TABLE_MANAGEMENT_KEYS.SELECT_TABLE_PLACEHOLDER, "테이블을 선택해주세요")} +

+
+ ) : ( + <> + {/* 중앙 헤더: 테이블명 + 라벨 입력 + 저장 */} +
+
+
+ {tableLabel || selectedTable} +
+
+ {selectedTable} +
+
+
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" + /> + setTableDescription(e.target.value)} + placeholder="설명" + className="h-8 max-w-[200px] text-xs" />
+
- {/* 테이블 목록 */} -
- {/* 전체 선택 및 일괄 삭제 (최고 관리자만) */} - {isSuperAdmin && ( -
-
- - 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="전체 선택" - /> - - {selectedTableIds.size > 0 && `${selectedTableIds.size}개 선택됨`} - -
- {selectedTableIds.size > 0 && ( - - )} -
- )} - - {loading ? ( -
- - - {getTextFromUI(TABLE_MANAGEMENT_KEYS.MESSAGE_LOADING_TABLES, "테이블 로딩 중...")} - -
- ) : tables.length === 0 ? ( -
- {getTextFromUI(TABLE_MANAGEMENT_KEYS.MESSAGE_NO_TABLES, "테이블이 없습니다")} -
- ) : ( - tables - .filter( - (table) => - table.tableName.toLowerCase().includes(searchTerm.toLowerCase()) || - (table.displayName && table.displayName.toLowerCase().includes(searchTerm.toLowerCase())), - ) - .map((table) => ( -
-
- {/* 체크박스 (최고 관리자만) */} - {isSuperAdmin && ( - handleTableCheck(table.tableName, checked as boolean)} - aria-label={`${table.displayName || table.tableName} 선택`} - className="mt-0.5" - onClick={(e) => e.stopPropagation()} - /> - )} -
handleTableSelect(table.tableName)}> -

{table.displayName || table.tableName}

-

- {table.description || getTextFromUI(TABLE_MANAGEMENT_KEYS.TABLE_DESCRIPTION, "설명 없음")} -

-
- 컬럼 - - {table.columnCount} - -
-
-
-
- )) - )} -
-
- } - right={ -
- {!selectedTable ? ( -
-
-

- {getTextFromUI(TABLE_MANAGEMENT_KEYS.SELECT_TABLE_PLACEHOLDER, "테이블을 선택해주세요")} -

-
+ {columnsLoading ? ( +
+ + + {getTextFromUI(TABLE_MANAGEMENT_KEYS.MESSAGE_LOADING_COLUMNS, "컬럼 정보 로딩 중...")} + +
+ ) : columns.length === 0 ? ( +
+ {getTextFromUI(TABLE_MANAGEMENT_KEYS.MESSAGE_NO_COLUMNS, "컬럼이 없습니다")}
) : ( <> - {/* 테이블 라벨 설정 + 저장 버튼 (고정 영역) */} -
-
- setTableLabel(e.target.value)} - placeholder="테이블 표시명" - className="h-10 text-sm" - /> -
-
- setTableDescription(e.target.value)} - placeholder="테이블 설명" - className="h-10 text-sm" - /> -
- {/* 저장 버튼 (항상 보이도록 상단에 배치) */} - -
- - {columnsLoading ? ( -
- - - {getTextFromUI(TABLE_MANAGEMENT_KEYS.MESSAGE_LOADING_COLUMNS, "컬럼 정보 로딩 중...")} - -
- ) : columns.length === 0 ? ( -
- {getTextFromUI(TABLE_MANAGEMENT_KEYS.MESSAGE_NO_COLUMNS, "컬럼이 없습니다")} -
- ) : ( -
-
- - { - 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} - /> -
- )} -
- )} + + { + const idx = columns.findIndex((c) => c.columnName === columnName); + if (idx >= 0) handleColumnChange(idx, field, value); + }} + constraints={constraints} + typeFilter={typeFilter} + getColumnIndexState={getColumnIndexState} + /> )} -
- } - leftTitle="테이블 목록" - leftWidth={20} - minLeftWidth={10} - maxLeftWidth={35} - height="100%" - className="flex-1 overflow-hidden" - /> + + )} +
+ + {/* 우측: 상세 패널 (selectedColumn 있을 때만) */} + {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} + /> +
+ )} +
{/* DDL 모달 컴포넌트들 */} {isSuperAdmin && ( @@ -1817,7 +1823,6 @@ export default function TableManagementPage() { {/* Scroll to Top 버튼 */} -
); } diff --git a/frontend/components/admin/table-type/ColumnDetailPanel.tsx b/frontend/components/admin/table-type/ColumnDetailPanel.tsx index 39914dbe..77f5dedf 100644 --- a/frontend/components/admin/table-type/ColumnDetailPanel.tsx +++ b/frontend/components/admin/table-type/ColumnDetailPanel.tsx @@ -100,75 +100,152 @@ export function ColumnDetailPanel({
{/* [섹션 1] 데이터 타입 선택 */}
-
- - +
+

이 필드는 어떤 유형인가요?

+

유형에 따라 입력 방식이 바뀌어요

-
- {Object.entries(INPUT_TYPE_COLORS).map(([type, conf]) => ( - - ))} +
+ {Object.entries(INPUT_TYPE_COLORS).map(([type, conf]) => { + const isSelected = (column.inputType || "text") === type; + return ( + + ); + })}
{/* [섹션 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" && ( +
+ + - + - 테이블을 찾을 수 없습니다. + 컬럼을 찾을 수 없습니다. - {refTableOpts.map((opt) => ( + { + onColumnChange("referenceColumn", undefined); + setEntityColumnOpen(false); + }} + className="text-xs" + > + + 선택 안함 + + {refColumns.map((refCol) => ( { - 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" > - {opt.label} + {refCol.columnName} ))} @@ -177,67 +254,20 @@ export function ColumnDetailPanel({
- {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.referenceTable && column.referenceTable !== "none" && column.referenceColumn && ( +
+ + {column.referenceTable} + + + + {column.referenceColumn} + +
+ )}
)} diff --git a/frontend/components/admin/table-type/ColumnGrid.tsx b/frontend/components/admin/table-type/ColumnGrid.tsx index e520fc43..5f339a8a 100644 --- a/frontend/components/admin/table-type/ColumnGrid.tsx +++ b/frontend/components/admin/table-type/ColumnGrid.tsx @@ -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 색상바 */} -
+ {/* 4px 색상바 (타입별 진한 색) */} +
{/* 라벨 + 컬럼명 */}
@@ -180,48 +181,72 @@ export function ColumnGrid({ {typeConf.label}
- {/* PK / NN / IDX / UQ (읽기 전용) */} + {/* PK / NN / IDX / UQ (클릭 토글) */}
- { + e.stopPropagation(); + onColumnChange(column.columnName, "isPrimaryKey" as keyof ColumnTypeInfo, !idxState.isPk); + }} + title="Primary Key 토글" > PK - - +
diff --git a/frontend/components/admin/table-type/types.ts b/frontend/components/admin/table-type/types.ts index 8adbcb62..329b4049 100644 --- a/frontend/components/admin/table-type/types.ts +++ b/frontend/components/admin/table-type/types.ts @@ -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 = { - 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: "◉" }, }; /** 컬럼 그룹 판별 */