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: "◉" }, }; /** 컬럼 그룹 판별 */